#![cfg(not(target_arch = "wasm32"))]
use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use kevy_embedded::{Config, Store};
static START_GATE: Mutex<()> = Mutex::new(());
mod tempdir {
use std::path::PathBuf;
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn new(label: &str) -> Self {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!("{label}-{nanos}"));
std::fs::create_dir_all(&path).unwrap();
Self { path }
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}
fn free_port_block(width: usize) -> u16 {
'retry: loop {
let anchor = TcpListener::bind("127.0.0.1:0").unwrap();
let base = anchor.local_addr().unwrap().port();
if base.checked_add(width as u16).is_none() {
continue;
}
let mut probes = Vec::with_capacity(width);
for i in 1..=width as u16 {
match TcpListener::bind(("127.0.0.1", base + i)) {
Ok(l) => probes.push(l),
Err(_) => continue 'retry,
}
}
return base;
}
}
struct Server {
port: u16,
replication_base: u16,
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
_dir: tempdir::TempDir,
}
impl Server {
fn start() -> Server {
let _gate = START_GATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
unsafe {
std::env::set_var("KEVY_IO_URING", "0");
}
let base = free_port_block(2);
let port = base;
let replication_base = base + 1;
let dir = tempdir::TempDir::new("kevy-embed-replica-e2e");
let dir_path = dir.path().to_path_buf();
let stop = Arc::new(AtomicBool::new(false));
let stop_t = stop.clone();
let handle = std::thread::spawn(move || {
let rt = kevy_rt::Runtime::new([127, 0, 0, 1], port, 1, kevy::KevyCommands)
.with_data_dir(dir_path)
.with_aof(false)
.with_replication(true, 1024 * 1024)
.with_replication_listener(replication_base);
let _ = rt.run(stop_t);
});
for p in [port, replication_base] {
let mut ready = false;
for _ in 0..400 {
if std::net::TcpStream::connect(("127.0.0.1", p)).is_ok() {
ready = true;
break;
}
std::thread::sleep(Duration::from_millis(5));
}
assert!(ready, "server did not come up on port {p}");
}
Server {
port,
replication_base,
stop,
handle: Some(handle),
_dir: dir,
}
}
fn shutdown(mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
fn cmd(&self, parts: &[&[u8]]) {
let mut s = std::net::TcpStream::connect(("127.0.0.1", self.port)).unwrap();
s.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
let mut req: Vec<u8> = Vec::new();
req.extend_from_slice(format!("*{}\r\n", parts.len()).as_bytes());
for p in parts {
req.extend_from_slice(format!("${}\r\n", p.len()).as_bytes());
req.extend_from_slice(p);
req.extend_from_slice(b"\r\n");
}
s.write_all(&req).unwrap();
let mut buf = [0u8; 64];
let n = s.read(&mut buf).unwrap_or(0);
assert!(n > 0, "no reply to {parts:?}");
let head = buf[0];
assert!(
head == b'+' || head == b'$' || head == b':',
"unexpected reply head {head:?} for {parts:?}: {:?}",
String::from_utf8_lossy(&buf[..n]),
);
}
}
fn wait_for<F: Fn() -> bool>(timeout: Duration, predicate: F) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if predicate() {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
false
}
#[test]
fn server_primary_streams_to_embed_replica() {
let server = Server::start();
let upstream = format!("127.0.0.1:{}", server.replication_base);
let replica = Store::open_replica(&upstream).unwrap();
assert!(replica.is_replica(), "open_replica did not set replica flag");
server.cmd(&[b"SET", b"key-a", b"hello"]);
server.cmd(&[b"SET", b"key-b", b"world"]);
let saw_both = wait_for(Duration::from_secs(5), || {
replica.get(b"key-a").unwrap().as_deref() == Some(b"hello".as_slice())
&& replica.get(b"key-b").unwrap().as_deref() == Some(b"world".as_slice())
});
assert!(
saw_both,
"embed replica never observed both SET writes within timeout"
);
drop(replica);
server.shutdown();
}
#[test]
fn embed_replica_rejects_local_writes_with_readonly() {
let cfg = Config::default()
.with_replica_upstream("127.0.0.1:1") .with_replica_reconnect(Duration::from_millis(50), Duration::from_millis(100));
let replica = Store::open(cfg).unwrap();
let err = replica.set(b"k", b"v").expect_err("write should be refused");
let msg = err.to_string();
assert!(
msg.contains("READONLY"),
"expected READONLY error, got: {msg}"
);
assert_eq!(replica.get(b"k").unwrap(), None);
assert!(replica.del(&[&b"k"[..]]).is_err());
assert!(replica.incr_by(b"n", 1).is_err());
assert!(replica.hset(b"h", &[(&b"f"[..], &b"v"[..])]).is_err());
assert!(replica.lpush(b"l", &[&b"x"[..]]).is_err());
assert!(replica.sadd(b"s", &[&b"m"[..]]).is_err());
assert!(replica.zadd(b"z", &[(1.0, &b"m"[..])]).is_err());
}
#[test]
fn embed_replica_streams_multiple_writes_on_same_connection() {
let server = Server::start();
let upstream = format!("127.0.0.1:{}", server.replication_base);
let cfg = Config::default()
.with_replica_upstream(&upstream)
.with_replica_reconnect(Duration::from_millis(30), Duration::from_millis(100));
let replica = Store::open(cfg).unwrap();
server.cmd(&[b"SET", b"k1", b"v1"]);
assert!(
wait_for(Duration::from_secs(5), || {
replica.get(b"k1").unwrap().as_deref() == Some(b"v1".as_slice())
}),
"embed replica missed first write"
);
server.cmd(&[b"SET", b"k2", b"v2"]);
assert!(
wait_for(Duration::from_secs(5), || {
replica.get(b"k2").unwrap().as_deref() == Some(b"v2".as_slice())
}),
"embed replica missed second write on the same connection"
);
drop(replica);
server.shutdown();
}
#[test]
fn fresh_embed_catches_up_against_existing_primary_backlog() {
let server = Server::start();
for i in 0..10 {
let k = format!("pre-{i}");
let v = format!("v-{i}");
server.cmd(&[b"SET", k.as_bytes(), v.as_bytes()]);
}
let upstream = format!("127.0.0.1:{}", server.replication_base);
let replica = Store::open_replica(&upstream).unwrap();
assert!(
wait_for(Duration::from_secs(5), || {
(0..10).all(|i| {
let k = format!("pre-{i}");
let v = format!("v-{i}");
replica.get(k.as_bytes()).unwrap().as_deref() == Some(v.as_bytes())
})
}),
"fresh embed never caught up with existing backlog"
);
server.cmd(&[b"SET", b"post-write", b"post-val"]);
assert!(
wait_for(Duration::from_secs(5), || {
replica.get(b"post-write").unwrap().as_deref() == Some(b"post-val".as_slice())
}),
"embed missed a write that arrived after catch-up"
);
drop(replica);
server.shutdown();
}
#[test]
fn embed_retargets_to_new_primary_via_set_replica_upstream() {
let server_a = Server::start();
let upstream_a = format!("127.0.0.1:{}", server_a.replication_base);
let replica = Store::open_replica(&upstream_a).unwrap();
server_a.cmd(&[b"SET", b"from-a", b"a-val"]);
assert!(
wait_for(Duration::from_secs(5), || {
replica.get(b"from-a").unwrap().as_deref() == Some(b"a-val".as_slice())
}),
"embed never observed write from primary A"
);
let server_b = Server::start();
let upstream_b = format!("127.0.0.1:{}", server_b.replication_base);
server_b.cmd(&[b"SET", b"from-b", b"b-val"]);
replica
.set_replica_upstream(&upstream_b)
.expect("retarget should succeed on a replica store");
assert!(
wait_for(Duration::from_secs(5), || {
replica.get(b"from-b").unwrap().as_deref() == Some(b"b-val".as_slice())
}),
"embed never observed write from primary B after retarget"
);
drop(replica);
server_a.shutdown();
server_b.shutdown();
}
#[test]
fn set_replica_upstream_on_non_replica_returns_error() {
let s = Store::open(Config::default()).unwrap();
assert!(!s.is_replica());
let err = s
.set_replica_upstream("127.0.0.1:1")
.expect_err("should error on a non-replica");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn embed_restart_resumes_via_fresh_handshake() {
let server = Server::start();
let upstream = format!("127.0.0.1:{}", server.replication_base);
{
let r1 = Store::open_replica(&upstream).unwrap();
server.cmd(&[b"SET", b"persist", b"a"]);
assert!(
wait_for(Duration::from_secs(5), || {
r1.get(b"persist").unwrap().as_deref() == Some(b"a".as_slice())
}),
"first embed never observed initial write"
);
drop(r1);
}
std::thread::sleep(Duration::from_millis(200));
let r2 = Store::open_replica(&upstream).unwrap();
assert!(
wait_for(Duration::from_secs(15), || {
r2.get(b"persist").unwrap().as_deref() == Some(b"a".as_slice())
}),
"restarted embed never re-applied the existing backlog via snapshot ship"
);
drop(r2);
server.shutdown();
}