pub mod reconnect;
pub mod tcp;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::{self, Cursor};
use std::rc::Rc;
use std::sync::Arc;
use self::tcp::{Error as TcpError, TcpStream};
use super::protocol::api::{Call, Eval, Execute, Ping, Request};
use super::protocol::{self, Error as ProtocolError, Protocol, SyncIndex};
use crate::fiber;
use crate::fiber::r#async::oneshot;
use crate::fiber::r#async::IntoOnDrop as _;
use crate::fiber::FiberId;
use crate::tuple::{ToTupleBuffer, Tuple};
use futures::{AsyncReadExt, AsyncWriteExt};
#[derive(thiserror::Error, Debug, Clone)]
pub enum Error {
#[error("{0}")]
Tcp(Arc<TcpError>),
#[error("{0}")]
Io(Arc<io::Error>),
#[error("protocol error: {0}")]
Protocol(Arc<ProtocolError>),
}
impl From<Error> for crate::error::Error {
fn from(err: Error) -> Self {
match err {
Error::Tcp(err) => crate::error::Error::Tcp(err),
Error::Io(err) => crate::error::Error::IO(err.kind().into()),
Error::Protocol(err) => crate::error::Error::Protocol(err),
}
}
}
impl From<TcpError> for Error {
fn from(err: TcpError) -> Self {
Error::Tcp(Arc::new(err))
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::Io(Arc::new(err))
}
}
impl From<ProtocolError> for Error {
fn from(err: ProtocolError) -> Self {
Error::Protocol(Arc::new(err))
}
}
#[derive(Clone, Debug)]
enum State {
Alive,
ClosedManually,
ClosedWithError(Error),
}
impl State {
fn is_alive(&self) -> bool {
matches!(self, Self::Alive)
}
fn is_closed(&self) -> bool {
!self.is_alive()
}
}
#[derive(Debug)]
struct ClientInner {
protocol: Protocol,
awaiting_response: HashMap<SyncIndex, oneshot::Sender<Result<(), Error>>>,
state: State,
stream: TcpStream,
sender_fiber_id: Option<FiberId>,
receiver_fiber_id: Option<FiberId>,
clients_count: usize,
}
impl ClientInner {
pub fn new(config: protocol::Config, stream: TcpStream) -> Self {
Self {
protocol: Protocol::with_config(config),
awaiting_response: HashMap::new(),
state: State::Alive,
stream,
sender_fiber_id: None,
receiver_fiber_id: None,
clients_count: 1,
}
}
}
fn maybe_wake_sender(client: &ClientInner) {
if client.protocol.ready_outgoing_len() == 0 {
return;
}
if let Some(id) = client.sender_fiber_id {
fiber::wakeup(id);
}
}
#[derive(Debug)]
pub struct Client(Rc<RefCell<ClientInner>>);
impl Client {
pub async fn connect(url: &str, port: u16) -> Result<Self, Error> {
Self::connect_with_config(url, port, Default::default()).await
}
pub async fn connect_with_config(
url: &str,
port: u16,
config: protocol::Config,
) -> Result<Self, Error> {
let stream = TcpStream::connect(url, port).await?;
let client = ClientInner::new(config, stream.clone());
let client = Rc::new(RefCell::new(client));
let receiver_fiber_id = fiber::Builder::new()
.func_async(receiver(client.clone(), stream.clone()))
.name(format!("iproto-in/{url}:{port}"))
.start_non_joinable()
.unwrap();
let sender_fiber_id = fiber::Builder::new()
.func_async(sender(client.clone(), stream))
.name(format!("iproto-out/{url}:{port}"))
.start_non_joinable()
.unwrap();
{
let mut client_mut = client.borrow_mut();
client_mut.receiver_fiber_id = Some(receiver_fiber_id);
client_mut.sender_fiber_id = Some(sender_fiber_id);
}
Ok(Self(client))
}
fn check_state(&self) -> Result<(), Error> {
match self.0.borrow().state.clone() {
State::Alive => Ok(()),
State::ClosedManually => unreachable!("All client handles are dropped at this point"),
State::ClosedWithError(err) => Err(err),
}
}
}
#[async_trait::async_trait(?Send)]
pub trait AsClient {
async fn send<R: Request>(&self, request: &R) -> Result<R::Response, Error>;
async fn ping(&self) -> Result<(), Error> {
self.send(&Ping).await
}
async fn call<T>(&self, fn_name: &str, args: &T) -> Result<Tuple, Error>
where
T: ToTupleBuffer + ?Sized,
{
self.send(&Call { fn_name, args }).await
}
async fn eval<T>(&self, expr: &str, args: &T) -> Result<Tuple, Error>
where
T: ToTupleBuffer + ?Sized,
{
self.send(&Eval { args, expr }).await
}
async fn execute<T>(
&self,
sql: &str,
bind_params: &T,
limit: Option<usize>,
) -> Result<Vec<Tuple>, Error>
where
T: ToTupleBuffer + ?Sized,
{
self.send(&Execute {
sql,
bind_params,
limit,
})
.await
}
}
#[async_trait::async_trait(?Send)]
impl AsClient for Client {
async fn send<R: Request>(&self, request: &R) -> Result<R::Response, Error> {
self.check_state()?;
let sync = self.0.borrow_mut().protocol.send_request(request)?;
let (tx, rx) = oneshot::channel();
self.0.borrow_mut().awaiting_response.insert(sync, tx);
maybe_wake_sender(&self.0.borrow());
rx.on_drop(|| {
let _ = self.0.borrow_mut().awaiting_response.remove(&sync);
})
.await
.expect("Channel should be open")?;
Ok(self
.0
.borrow_mut()
.protocol
.take_response(sync, request)
.expect("Is present at this point")?)
}
}
impl Drop for Client {
fn drop(&mut self) {
let clients_count = self.0.borrow().clients_count;
if clients_count == 1 {
let mut client = self.0.borrow_mut();
client.state = State::ClosedManually;
let receiver_fiber_id = client.receiver_fiber_id;
let sender_fiber_id = client.sender_fiber_id;
if let Err(e) = client.stream.close() {
crate::log::say(
crate::log::SayLevel::Error,
file!(),
line!() as _,
None,
&format!("Client::drop: failed closing tcp stream: {e}"),
)
}
drop(client);
if let Some(id) = receiver_fiber_id {
fiber::cancel(id);
fiber::wakeup(id);
}
if let Some(id) = sender_fiber_id {
fiber::cancel(id);
fiber::wakeup(id);
}
} else {
self.0.borrow_mut().clients_count -= 1;
}
}
}
impl Clone for Client {
fn clone(&self) -> Self {
self.0.borrow_mut().clients_count += 1;
Self(self.0.clone())
}
}
macro_rules! handle_result {
($client:expr, $e:expr) => {
match $e {
Ok(value) => value,
Err(err) => {
let err: Error = err.into();
$client.state = State::ClosedWithError(err.clone());
let subscriptions: HashMap<_, _> = $client.awaiting_response.drain().collect();
for (_, subscription) in subscriptions {
let _ = subscription.send(Err(err.clone()));
}
return;
}
}
};
}
async fn sender(client: Rc<RefCell<ClientInner>>, mut writer: TcpStream) {
loop {
if client.borrow().state.is_closed() || fiber::is_cancelled() {
return;
}
let data = client.borrow_mut().protocol.take_outgoing_data();
if data.is_empty() {
fiber::fiber_yield();
} else {
let result = writer.write_all(&data).await;
handle_result!(client.borrow_mut(), result);
}
}
}
#[allow(clippy::await_holding_refcell_ref)]
async fn receiver(client_cell: Rc<RefCell<ClientInner>>, mut reader: TcpStream) {
let mut buf = vec![0_u8; 4096];
loop {
let client = client_cell.borrow();
if client.state.is_closed() || fiber::is_cancelled() {
return;
}
let size = client.protocol.read_size_hint();
if buf.len() < size {
buf.resize(size, 0);
}
let buf_slice = &mut buf[0..size];
drop(client);
let res = reader.read_exact(buf_slice).await;
let mut client = client_cell.borrow_mut();
handle_result!(client, res);
let result = client
.protocol
.process_incoming(&mut Cursor::new(buf_slice));
let result = handle_result!(client, result);
if let Some(sync) = result {
let subscription = client.awaiting_response.remove(&sync);
if let Some(subscription) = subscription {
subscription
.send(Ok(()))
.expect("cannot be closed at this point");
} else {
crate::log::say(
crate::log::SayLevel::Warn,
file!(),
line!() as _,
None,
&format!("received unwaited message for {sync:?}"),
)
}
}
maybe_wake_sender(&client);
}
}
#[cfg(feature = "internal_test")]
mod tests {
use super::*;
use crate::fiber::r#async::timeout::IntoTimeout as _;
use crate::space::Space;
use crate::test::util::listen_port;
use std::time::Duration;
async fn test_client() -> Client {
Client::connect_with_config(
"localhost",
listen_port(),
protocol::Config {
creds: Some(("test_user".into(), "password".into())),
},
)
.timeout(Duration::from_secs(3))
.await
.unwrap()
}
#[crate::test(tarantool = "crate")]
async fn connect() {
let _client = Client::connect("localhost", listen_port()).await.unwrap();
}
#[crate::test(tarantool = "crate")]
async fn connect_failure() {
let err = Client::connect("localhost", 0).await.unwrap_err();
assert!(matches!(dbg!(err), Error::Tcp(_)))
}
#[crate::test(tarantool = "crate")]
async fn ping() {
let client = test_client().await;
for _ in 0..5 {
client.ping().timeout(Duration::from_secs(3)).await.unwrap();
}
}
#[crate::test(tarantool = "crate")]
fn ping_concurrent() {
let client = fiber::block_on(test_client());
let fiber_a = fiber::start_async(async {
client.ping().timeout(Duration::from_secs(3)).await.unwrap()
});
let fiber_b = fiber::start_async(async {
client.ping().timeout(Duration::from_secs(3)).await.unwrap()
});
fiber_a.join();
fiber_b.join();
}
#[crate::test(tarantool = "crate")]
async fn execute() {
Space::find("test_s1")
.unwrap()
.insert(&(6001, "6001"))
.unwrap();
Space::find("test_s1")
.unwrap()
.insert(&(6002, "6002"))
.unwrap();
let client = test_client().await;
let lua = crate::lua_state();
_ = lua.exec("require'compat'.sql_seq_scan_default = 'old'");
let result = client
.execute(r#"SELECT * FROM "test_s1""#, &(), None)
.timeout(Duration::from_secs(3))
.await
.unwrap();
assert!(result.len() >= 2);
let result = client
.execute(r#"SELECT * FROM "test_s1" WHERE "id" = ?"#, &(6002,), None)
.timeout(Duration::from_secs(3))
.await
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(
result.get(0).unwrap().decode::<(u64, String)>().unwrap(),
(6002, "6002".into())
);
}
#[crate::test(tarantool = "crate")]
async fn call() {
let client = test_client().await;
let result = client
.call("test_stored_proc", &(1, 2))
.timeout(Duration::from_secs(3))
.await
.unwrap();
assert_eq!(result.decode::<(i32,)>().unwrap(), (3,));
}
#[crate::test(tarantool = "crate")]
async fn invalid_call() {
let client = test_client().await;
let err = client
.call("unexistent_proc", &())
.timeout(Duration::from_secs(3))
.await
.unwrap_err()
.to_string();
assert_eq!(err, "protocol error: service responded with error: Procedure 'unexistent_proc' is not defined");
}
#[crate::test(tarantool = "crate")]
async fn eval() {
let client = test_client().await;
let result = client
.eval("return ...", &(1, 2))
.timeout(Duration::from_secs(3))
.await
.unwrap();
assert_eq!(result.decode::<(i32, i32)>().unwrap(), (1, 2));
}
#[crate::test(tarantool = "crate")]
async fn client_count_regression() {
let client = test_client().await;
client.0.borrow_mut().stream.close().unwrap();
fiber::reschedule();
let fiber_id = client.0.borrow().sender_fiber_id.unwrap();
let fiber_exists = fiber::wakeup(fiber_id);
debug_assert!(fiber_exists);
fiber::reschedule();
assert_eq!(Rc::strong_count(&client.0), 1);
let client_clone = client.clone();
assert_eq!(Rc::strong_count(&client.0), 2);
drop(client_clone);
assert_eq!(Rc::strong_count(&client.0), 1);
client.check_state().unwrap_err();
}
#[crate::test(tarantool = "crate")]
async fn concurrent_messages_one_fiber() {
let client = test_client().await;
let mut ping_futures = vec![];
for _ in 0..10 {
ping_futures.push(client.ping());
}
for res in futures::future::join_all(ping_futures).await {
res.unwrap();
}
}
#[crate::test(tarantool = "crate")]
async fn data_always_present_in_response() {
let client = test_client().await;
client.eval("return", &()).await.unwrap();
client.call("LUA", &("return",)).await.unwrap();
}
#[crate::test(tarantool = "crate")]
async fn big_data() {
use crate::tuple::RawByteBuf;
#[crate::proc(tarantool = "crate")]
fn proc_big_data<'a>(s: &'a serde_bytes::Bytes) -> usize {
s.len() + 17
}
let path = crate::proc::module_path(&big_data as *const _ as _).unwrap();
let module = path.file_stem().unwrap();
let module = module.to_str().unwrap();
let proc = format!("{module}.proc_big_data");
let lua = crate::lua_state();
lua.exec_with("box.schema.func.create(..., { language = 'C' })", &proc)
.unwrap();
let client = test_client().await;
const N: u32 = 0x6fff_ff69;
#[allow(clippy::uninit_vec)]
let s = unsafe {
let buf_size = (N + 6) as usize;
let mut data = Vec::<u8>::with_capacity(buf_size);
data.set_len(buf_size);
data[0] = b'\x91';
data[1] = b'\xc6'; data[2..6].copy_from_slice(&N.to_be_bytes());
RawByteBuf::from(data)
};
let t0 = std::time::Instant::now();
let t = client.call(&proc, &s).await.unwrap();
dbg!(t0.elapsed());
if let Ok((len,)) = t.decode::<(u32,)>() {
assert_eq!(len, N + 17);
} else {
let ((len,),): ((u32,),) = t.decode().unwrap();
assert_eq!(len, N + 17);
}
}
}