use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::ffi::{c_void, CString};
use std::marker::PhantomData;
use rayforce_sys as sys;
use crate::error::{check, RayError, Result};
use crate::poll::Poll;
use crate::runtime::assert_on_runtime_thread;
use crate::value::Value;
pub struct QConnection {
fd: i32,
_not_send: PhantomData<*mut ()>,
}
impl QConnection {
pub fn connect(host: &str, port: u16) -> Result<Self> {
Self::connect_with(host, port, "", "", 0)
}
pub fn connect_with(
host: &str,
port: u16,
user: &str,
password: &str,
timeout_ms: i32,
) -> Result<Self> {
assert_on_runtime_thread("QConnection::connect");
let host_c = CString::new(host).map_err(|_| RayError::binding("Q host contains NUL"))?;
let user_c = CString::new(user).map_err(|_| RayError::binding("Q user contains NUL"))?;
let pass_c =
CString::new(password).map_err(|_| RayError::binding("Q password contains NUL"))?;
let fd = unsafe {
sys::q_connect(
host_c.as_ptr(),
i32::from(port),
user_c.as_ptr(),
pass_c.as_ptr(),
timeout_ms,
)
};
if fd < 0 {
let reason = match fd {
sys::Q_ERR_TIMEOUT => "timed out",
sys::Q_ERR_HANDSHAKE => "handshake/auth failed",
_ => "connection failed",
};
return Err(RayError::binding(format!(
"Q: connect to {host}:{port} {reason}"
)));
}
Ok(QConnection {
fd,
_not_send: PhantomData,
})
}
pub fn execute(&self, query: &str) -> Result<Value> {
let q = Value::string(query);
let mut err = [0 as std::os::raw::c_char; 256];
let res = unsafe { sys::q_send(self.fd, q.as_ptr(), err.as_mut_ptr(), err.len()) };
if res.is_null() {
let msg = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }.to_string_lossy();
let msg = if msg.is_empty() {
"q_send failed".into()
} else {
msg
};
return Err(RayError::binding(format!("Q: {msg}")));
}
let ok = unsafe { check(res)? };
Ok(unsafe { Value::from_owned(ok) })
}
}
thread_local! {
static ATTACHED: RefCell<HashMap<i64, u64>> = RefCell::new(HashMap::new());
static NEXT_TOKEN: Cell<u64> = const { Cell::new(1) };
}
fn claim_selector(id: i64) -> u64 {
let token = NEXT_TOKEN.with(|n| {
let t = n.get();
n.set(t + 1);
t
});
ATTACHED.with(|a| a.borrow_mut().insert(id, token));
token
}
fn selector_is_claimed_by(id: i64, token: u64) -> bool {
ATTACHED.with(|a| a.borrow().get(&id) == Some(&token))
}
fn release_selector(id: i64, token: u64) {
ATTACHED.with(|a| {
let mut a = a.borrow_mut();
if a.get(&id) == Some(&token) {
a.remove(&id);
}
});
}
impl QConnection {
pub fn attach(self, poll: &Poll) -> Result<Subscription<'_>> {
assert_on_runtime_thread("QConnection::attach");
let fd = self.fd;
std::mem::forget(self);
let id = unsafe { sys::q_conn_attach(poll.as_ptr(), fd) };
if id < 0 {
unsafe { sys::q_close(fd) };
return Err(RayError::binding("Q: could not attach to the event loop"));
}
let Some(data) = poll.selector_data(id) else {
unsafe { sys::q_conn_close(poll.as_ptr(), id) };
return Err(RayError::binding("Q: attached selector did not resolve"));
};
let token = claim_selector(id);
Ok(Subscription {
poll,
id,
data,
token,
})
}
}
impl Drop for QConnection {
fn drop(&mut self) {
unsafe { sys::q_close(self.fd) };
}
}
impl Poll {
pub fn serve_q(&self, port: u16) -> Result<QListener<'_>> {
assert_on_runtime_thread("Poll::serve_q");
let id = unsafe { sys::q_serve(self.as_ptr(), i32::from(port)) };
if id < 0 {
return Err(RayError::binding(format!(
"Q: could not listen on port {port}"
)));
}
Ok(QListener {
id,
port,
_poll: PhantomData,
_not_send: PhantomData,
})
}
}
pub struct QListener<'p> {
id: i64,
port: u16,
_poll: PhantomData<&'p Poll>,
_not_send: PhantomData<*mut ()>,
}
impl QListener<'_> {
pub fn port(&self) -> u16 {
self.port
}
pub fn selector_id(&self) -> i64 {
self.id
}
}
pub struct Subscription<'p> {
poll: &'p Poll,
id: i64,
data: *mut c_void,
token: u64,
}
impl Subscription<'_> {
pub fn send(&self, msg: &Value) -> Result<Value> {
if !self.is_alive() {
return Err(RayError::binding("Q: subscription is closed"));
}
let res = unsafe { sys::q_conn_send(self.poll.as_ptr(), self.id, msg.as_ptr()) };
if res.is_null() {
return Err(RayError::binding(
"Q: send on the attached connection failed",
));
}
let ok = unsafe { check(res)? };
Ok(unsafe { Value::from_owned(ok) })
}
pub fn execute(&self, expr: &str) -> Result<Value> {
self.send(&Value::string(expr))
}
pub fn is_alive(&self) -> bool {
selector_is_claimed_by(self.id, self.token) && self.poll.holds(self.id, self.data)
}
pub fn close(self) {
}
}
impl Drop for Subscription<'_> {
fn drop(&mut self) {
if self.is_alive() {
unsafe { sys::q_conn_close(self.poll.as_ptr(), self.id) };
}
release_selector(self.id, self.token);
}
}
pub fn encode(msg: &Value) -> Result<Vec<u8>> {
let mut buf: *mut u8 = std::ptr::null_mut();
let mut len: i64 = 0;
let mut err = [0 as std::os::raw::c_char; 256];
let rc = unsafe {
sys::q_encode(
msg.as_ptr(),
&mut buf,
&mut len,
err.as_mut_ptr(),
err.len(),
)
};
if rc < 0 || buf.is_null() {
let reason = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }.to_string_lossy();
let reason = if reason.is_empty() {
"q_encode failed".into()
} else {
reason
};
return Err(RayError::binding(format!("Q: {reason}")));
}
let out = unsafe { std::slice::from_raw_parts(buf, len as usize) }.to_vec();
unsafe { libc_free(buf.cast()) };
Ok(out)
}
unsafe fn libc_free(p: *mut std::ffi::c_void) {
extern "C" {
fn free(p: *mut std::ffi::c_void);
}
free(p);
}
pub fn decode_response(msg: &[u8]) -> Result<Value> {
assert_on_runtime_thread("q::decode_response");
const HEADER_LEN: usize = 8;
if msg.len() < HEADER_LEN {
return Err(RayError::binding("Q: message shorter than wire header"));
}
if msg[0] != 1 {
return Err(RayError::binding(
"Q: only little-endian response messages are supported",
));
}
if msg[1] != 2 {
return Err(RayError::binding(format!(
"Q: expected response message type 2, got {}",
msg[1]
)));
}
let size = u32::from_le_bytes([msg[4], msg[5], msg[6], msg[7]]) as usize;
if size != msg.len() {
return Err(RayError::binding(
"Q: message length does not match wire header",
));
}
let compressed = i32::from(msg[2] != 0);
let body = &msg[HEADER_LEN..];
if body.is_empty() {
return Err(RayError::binding("Q: empty response body"));
}
let mut err = [0i8; 256];
let res = unsafe {
sys::q_decode(
body.as_ptr() as *mut u8,
body.len() as i64,
compressed,
err.as_mut_ptr(),
err.len(),
)
};
if res.is_null() {
let msg = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }.to_string_lossy();
let msg = if msg.is_empty() {
"q_decode failed".into()
} else {
msg
};
return Err(RayError::binding(format!("Q: {msg}")));
}
let ok = unsafe { check(res)? };
Ok(unsafe { Value::from_owned(ok) })
}