use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use reddb_server::wire::postgres::{start_pg_wire_listener, PgWireConfig};
use reddb_server::{RedDBOptions, RedDBRuntime};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
const EXCHANGE_TIMEOUT: Duration = Duration::from_secs(20);
struct ServerHandle {
addr: SocketAddr,
runtime: Arc<RedDBRuntime>,
_join: tokio::task::JoinHandle<()>,
}
async fn start_server() -> ServerHandle {
let probe = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = probe.local_addr().unwrap();
drop(probe);
let runtime = Arc::new(RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap());
let cfg = PgWireConfig {
bind_addr: addr.to_string(),
..PgWireConfig::default()
};
let runtime_for_task = Arc::clone(&runtime);
let join = tokio::spawn(async move {
let _ = start_pg_wire_listener(cfg, runtime_for_task).await;
});
sleep(Duration::from_millis(50)).await;
ServerHandle {
addr,
runtime,
_join: join,
}
}
async fn connect(addr: SocketAddr) -> TcpStream {
let mut stream = TcpStream::connect(addr).await.expect("connect pg wire");
write_startup(&mut stream).await;
read_until_ready(&mut stream).await;
stream
}
async fn simple_query(stream: &mut TcpStream, sql: &str) -> Vec<(u8, Vec<u8>)> {
write_frontend_frame(stream, b'Q', query_body(sql)).await;
timeout(EXCHANGE_TIMEOUT, read_until_ready(stream))
.await
.unwrap_or_else(|_| panic!("pg-wire exchange timed out for: {sql}"))
}
async fn exec_ok(stream: &mut TcpStream, sql: &str) -> Vec<(u8, Vec<u8>)> {
let frames = simple_query(stream, sql).await;
if let Some((_, body)) = frames.iter().find(|(tag, _)| *tag == b'E') {
panic!(
"{sql}: unexpected error frame: {:?}",
decode_error_message(body)
);
}
frames
}
fn data_row_count(frames: &[(u8, Vec<u8>)]) -> usize {
frames.iter().filter(|(tag, _)| *tag == b'D').count()
}
fn error_message(frames: &[(u8, Vec<u8>)]) -> Option<String> {
frames
.iter()
.find(|(tag, _)| *tag == b'E')
.and_then(|(_, body)| decode_error_message(body))
}
fn command_complete_tags(frames: &[(u8, Vec<u8>)]) -> Vec<String> {
frames
.iter()
.filter(|(tag, _)| *tag == b'C')
.map(|(_, body)| decode_command_complete(body).to_string())
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pg_wire_wait_returns_empty_after_budget_when_queue_stays_empty() {
let h = start_server().await;
let mut stream = connect(h.addr).await;
exec_ok(&mut stream, "CREATE QUEUE qpg_empty").await;
exec_ok(&mut stream, "QUEUE GROUP CREATE qpg_empty workers").await;
let started = Instant::now();
let frames = exec_ok(
&mut stream,
"QUEUE READ qpg_empty GROUP workers CONSUMER c1 COUNT 1 WAIT 1s",
)
.await;
let elapsed = started.elapsed();
assert_eq!(
data_row_count(&frames),
0,
"WAIT timeout should deliver an empty projection over PG-wire, frames={:?}",
frame_tags(&frames)
);
assert!(
command_complete_tags(&frames)
.iter()
.any(|tag| tag == "SELECT 0"),
"empty WAIT should complete as SELECT 0, tags={:?}",
command_complete_tags(&frames)
);
assert!(
elapsed >= Duration::from_millis(900),
"round-trip should park at least ~the WAIT budget, elapsed={elapsed:?}"
);
assert!(
elapsed < Duration::from_secs(4),
"round-trip should not stall past the budget, elapsed={elapsed:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pg_wire_enqueue_during_wait_delivers_message_to_waiter() {
let h = start_server().await;
let mut waiter = connect(h.addr).await;
exec_ok(&mut waiter, "CREATE QUEUE qpg_wake").await;
exec_ok(&mut waiter, "QUEUE GROUP CREATE qpg_wake workers").await;
let producer_addr = h.addr;
let producer = tokio::spawn(async move {
let mut producer = connect(producer_addr).await;
sleep(Duration::from_millis(150)).await;
exec_ok(&mut producer, "QUEUE PUSH qpg_wake 'wakeup'").await;
});
let started = Instant::now();
let frames = exec_ok(
&mut waiter,
"QUEUE READ qpg_wake GROUP workers CONSUMER c1 COUNT 1 WAIT 5s",
)
.await;
let elapsed = started.elapsed();
producer.await.expect("producer joined");
assert_eq!(
data_row_count(&frames),
1,
"committed enqueue must wake the waiter and deliver over PG-wire, frames={:?}",
frame_tags(&frames)
);
let delivered = frames
.iter()
.find(|(tag, _)| *tag == b'D')
.map(|(_, body)| body.clone())
.expect("a DataRow frame");
assert!(
delivered.windows(b"wakeup".len()).any(|w| w == b"wakeup"),
"delivered payload should round-trip in the DataRow bytes"
);
assert!(
elapsed < Duration::from_secs(3),
"post-commit notify should wake well before the 5s budget (elapsed={elapsed:?})"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pg_wire_wait_above_cap_is_rejected_with_explicit_error() {
let h = start_server().await;
let mut stream = connect(h.addr).await;
exec_ok(&mut stream, "CREATE QUEUE qpg_cap").await;
exec_ok(&mut stream, "QUEUE GROUP CREATE qpg_cap workers").await;
let started = Instant::now();
let frames = simple_query(
&mut stream,
"QUEUE READ qpg_cap GROUP workers CONSUMER c1 COUNT 1 WAIT 999h",
)
.await;
let elapsed = started.elapsed();
let msg = error_message(&frames).unwrap_or_else(|| {
panic!(
"WAIT > cap should surface an ErrorResponse, frames={:?}",
frame_tags(&frames)
)
});
assert!(
msg.contains("red.config.queue.max_wait_ms"),
"rejection should name the cap key over PG-wire, msg={msg}"
);
assert!(
msg.contains("60000"),
"rejection should name the active cap value (default 60000ms), msg={msg}"
);
assert!(
elapsed < Duration::from_secs(1),
"cap rejection must not park before refusing, elapsed={elapsed:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pg_wire_wait_cancellation_returns_explicit_error_not_empty_timeout() {
let h = start_server().await;
let mut stream = connect(h.addr).await;
exec_ok(&mut stream, "CREATE QUEUE qpg_cancel").await;
exec_ok(&mut stream, "QUEUE GROUP CREATE qpg_cancel workers").await;
let registry = Arc::clone(&h.runtime.queue_wait_registry());
let canceler = tokio::spawn(async move {
sleep(Duration::from_millis(150)).await;
registry.cancel_all();
});
let frames = simple_query(
&mut stream,
"QUEUE READ qpg_cancel GROUP workers CONSUMER c1 COUNT 1 WAIT 5s",
)
.await;
canceler.await.expect("canceler joined");
let msg = error_message(&frames).unwrap_or_else(|| {
panic!(
"cancellation should surface an ErrorResponse, not a SELECT 0 reply, frames={:?}",
frame_tags(&frames)
)
});
assert!(
msg.contains("WAIT cancelled") || msg.contains("shutting down"),
"cancellation error should be explicit over PG-wire, msg={msg}"
);
assert_eq!(
data_row_count(&frames),
0,
"cancellation must not deliver rows, frames={:?}",
frame_tags(&frames)
);
h.runtime.queue_wait_registry().reset_cancelled();
}
fn frame_tags(frames: &[(u8, Vec<u8>)]) -> Vec<char> {
frames.iter().map(|(tag, _)| *tag as char).collect()
}
async fn write_startup<W: AsyncWrite + Unpin>(stream: &mut W) {
let mut payload = Vec::new();
payload.extend_from_slice(&((3u32) << 16).to_be_bytes());
payload.extend_from_slice(b"user\0reddb\0");
payload.push(0);
let len = (payload.len() + 4) as u32;
stream.write_all(&len.to_be_bytes()).await.unwrap();
stream.write_all(&payload).await.unwrap();
}
async fn write_frontend_frame<W: AsyncWrite + Unpin>(stream: &mut W, tag: u8, payload: Vec<u8>) {
stream.write_all(&[tag]).await.unwrap();
stream
.write_all(&((payload.len() + 4) as u32).to_be_bytes())
.await
.unwrap();
stream.write_all(&payload).await.unwrap();
}
async fn read_backend_frame<R: AsyncRead + Unpin>(stream: &mut R) -> (u8, Vec<u8>) {
let mut tag = [0u8; 1];
stream.read_exact(&mut tag).await.unwrap();
let mut len = [0u8; 4];
stream.read_exact(&mut len).await.unwrap();
let len = u32::from_be_bytes(len) as usize;
let mut body = vec![0u8; len - 4];
stream.read_exact(&mut body).await.unwrap();
(tag[0], body)
}
async fn read_until_ready<R: AsyncRead + Unpin>(stream: &mut R) -> Vec<(u8, Vec<u8>)> {
let mut frames = Vec::new();
loop {
let frame = read_backend_frame(stream).await;
let done = frame.0 == b'Z';
frames.push(frame);
if done {
return frames;
}
}
}
fn query_body(query: &str) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(query.as_bytes());
out.push(0);
out
}
fn decode_command_complete(body: &[u8]) -> &str {
let nul = body.iter().position(|&b| b == 0).unwrap_or(body.len());
std::str::from_utf8(&body[..nul]).unwrap()
}
fn decode_error_message(body: &[u8]) -> Option<String> {
let mut pos = 0;
while pos < body.len() {
let field = body[pos];
pos += 1;
if field == 0 {
break;
}
let end = body[pos..].iter().position(|&b| b == 0)? + pos;
let value = std::str::from_utf8(&body[pos..end]).ok()?.to_string();
pos = end + 1;
if field == b'M' {
return Some(value);
}
}
None
}