use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
static START_GATE: Mutex<()> = Mutex::new(());
fn free_port() -> u16 {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.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_exact_bytes(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 {:?}, got {:?}",
String::from_utf8_lossy(expected),
String::from_utf8_lossy(&buf),
);
}
fn read_resp_line(s: &mut std::net::TcpStream) -> Vec<u8> {
let mut out = Vec::new();
let mut prev = 0u8;
let mut byte = [0u8; 1];
loop {
s.read_exact(&mut byte).unwrap();
out.push(byte[0]);
if prev == b'\r' && byte[0] == b'\n' {
return out;
}
prev = byte[0];
}
}
fn read_resp_value(s: &mut std::net::TcpStream) -> Vec<u8> {
let line = read_resp_line(s);
let mut out = line.clone();
match line.first() {
Some(b'+' | b'-' | b':') => out,
Some(b'$') => {
let n: i64 = std::str::from_utf8(&line[1..line.len() - 2])
.unwrap()
.parse()
.unwrap();
if n < 0 {
return out;
}
let mut body = vec![0u8; n as usize + 2];
s.read_exact(&mut body).unwrap();
out.extend_from_slice(&body);
out
}
Some(b'*') => {
let n: i64 = std::str::from_utf8(&line[1..line.len() - 2])
.unwrap()
.parse()
.unwrap();
if n < 0 {
return out;
}
for _ in 0..n {
out.extend_from_slice(&read_resp_value(s));
}
out
}
_ => panic!("unexpected RESP prefix: {:?}", String::from_utf8_lossy(&line)),
}
}
struct Server {
port: u16,
dir: std::path::PathBuf,
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Server {
fn start(slower_than_micros: i64, max_len: u32, nshards: usize) -> Self {
let _gate = START_GATE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let port = free_port();
let dir = std::env::temp_dir().join(format!(
"kevy-slowlog-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let dir_thread = dir.clone();
let handle = std::thread::spawn(move || {
let rt = kevy_rt::Runtime::new([127, 0, 0, 1], port, nshards, kevy::KevyCommands)
.with_data_dir(dir_thread)
.with_aof(false)
.with_slowlog(slower_than_micros, max_len);
rt.run(stop_thread).unwrap();
});
let mut ready = false;
for _ in 0..200 {
if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(ready, "runtime did not come up");
Self {
port,
dir,
stop,
handle: Some(handle),
}
}
fn connect(&self) -> std::net::TcpStream {
let s = std::net::TcpStream::connect(("127.0.0.1", self.port)).unwrap();
s.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
s
}
}
impl Drop for Server {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn slowlog_len(c: &mut std::net::TcpStream) -> i64 {
c.write_all(&req(&[b"SLOWLOG", b"LEN"])).unwrap();
let line = read_resp_line(c);
assert_eq!(line[0], b':', "SLOWLOG LEN expected :N reply");
std::str::from_utf8(&line[1..line.len() - 2])
.unwrap()
.parse()
.unwrap()
}
#[test]
fn slowlog_help_returns_static_array() {
let srv = Server::start(10_000, 128, 1);
let mut c = srv.connect();
c.write_all(&req(&[b"SLOWLOG", b"HELP"])).unwrap();
let reply = read_resp_value(&mut c);
assert!(reply.starts_with(b"*"), "HELP must return an array");
let body = String::from_utf8_lossy(&reply);
assert!(
body.contains("SLOWLOG <subcommand>"),
"HELP synopsis missing: {body}"
);
assert!(body.contains("Reset the slowlog."), "HELP body missing");
}
#[test]
fn slowlog_get_len_reset_basic_flow_records_everything_when_threshold_zero() {
let srv = Server::start(0, 128, 1);
let mut c = srv.connect();
for i in 0..5u32 {
let key = format!("k{i}");
c.write_all(&req(&[b"SET", key.as_bytes(), b"v"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
assert!(slowlog_len(&mut c) >= 5, "expected ≥5 entries after 5 SETs");
c.write_all(&req(&[b"SLOWLOG", b"GET"])).unwrap();
let reply = read_resp_value(&mut c);
assert!(reply.starts_with(b"*"), "GET expected array");
let body = String::from_utf8_lossy(&reply);
assert!(body.contains("SET"), "GET reply missing SET argv: {body}");
c.write_all(&req(&[b"SLOWLOG", b"RESET"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
assert_eq!(slowlog_len(&mut c), 0, "RESET must zero the ring");
}
#[test]
fn slowlog_threshold_high_records_nothing() {
let srv = Server::start(i64::MAX, 128, 1);
let mut c = srv.connect();
for i in 0..10u32 {
let key = format!("t{i}");
c.write_all(&req(&[b"SET", key.as_bytes(), b"v"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
assert_eq!(
slowlog_len(&mut c),
0,
"threshold = i64::MAX must reject every entry"
);
}
#[test]
fn slowlog_off_skips_clock_and_records_nothing() {
let srv = Server::start(-1, 128, 1);
let mut c = srv.connect();
for i in 0..10u32 {
let key = format!("off{i}");
c.write_all(&req(&[b"SET", key.as_bytes(), b"v"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
assert_eq!(slowlog_len(&mut c), 0, "OFF must record nothing");
}
#[test]
fn slowlog_aggregates_across_shards() {
let srv = Server::start(0, 256, 4);
let mut c = srv.connect();
let n = 60u32;
for i in 0..n {
let key = format!("xs{i}");
c.write_all(&req(&[b"SET", key.as_bytes(), b"v"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
let total = slowlog_len(&mut c);
assert!(
total >= i64::from(n),
"expected ≥{n} SLOWLOG entries across 4 shards, got {total}"
);
c.write_all(&req(&[b"SLOWLOG", b"GET"])).unwrap();
let reply = read_resp_value(&mut c);
let body = String::from_utf8_lossy(&reply);
assert!(reply.starts_with(b"*"), "GET expected array");
assert!(body.contains("SET"), "GET reply missing SET argv");
c.write_all(&req(&[b"SLOWLOG", b"RESET"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
assert_eq!(slowlog_len(&mut c), 0, "RESET must clear every shard");
}
#[test]
fn slowlog_max_len_evicts_oldest() {
let srv = Server::start(0, 3, 1);
let mut c = srv.connect();
for i in 0..10u32 {
let key = format!("e{i}");
c.write_all(&req(&[b"SET", key.as_bytes(), b"v"])).unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
let len = slowlog_len(&mut c);
assert_eq!(len, 3, "max_len = 3 must cap the ring at 3 entries");
}
#[test]
fn slowlog_unknown_subcommand_returns_error() {
let srv = Server::start(10_000, 128, 1);
let mut c = srv.connect();
c.write_all(&req(&[b"SLOWLOG", b"BOGUS"])).unwrap();
let line = read_resp_line(&mut c);
assert_eq!(line[0], b'-', "unknown subcommand must be an error reply");
let body = String::from_utf8_lossy(&line);
assert!(body.contains("Unknown SLOWLOG subcommand"), "error body: {body}");
}
#[test]
fn slowlog_get_with_count_truncates() {
let srv = Server::start(0, 128, 1);
let mut c = srv.connect();
for i in 0..7u32 {
c.write_all(&req(&[b"SET", format!("tr{i}").as_bytes(), b"v"]))
.unwrap();
read_exact_bytes(&mut c, b"+OK\r\n");
}
c.write_all(&req(&[b"SLOWLOG", b"GET", b"3"])).unwrap();
let reply = read_resp_value(&mut c);
assert!(reply.starts_with(b"*3\r\n"), "GET 3 expected *3\\r\\n header, got {:?}",
String::from_utf8_lossy(&reply[..reply.len().min(16)]));
}