use std::sync::Arc;
use crate::error::{Error, Result};
use crate::tcp::connection_actor::ConnectionHandle;
use crate::tcp::cursor::TcpRawCursor;
use crate::tcp::pool::NativePool;
use crate::tcp::retry::{RetryPolicy, map_pool_error, run_with_retry};
pub const DEFAULT_TCP_INSERT_BLOCK_ROWS: u64 = 1_000_000;
pub async fn execute_query_via_pool(
pool: &Arc<NativePool>,
query_id: &str,
query: &str,
settings: &[(String, String)],
params: &[(String, String)],
retry: Option<RetryPolicy>,
idempotent: bool,
) -> Result<()> {
let effective = if idempotent { retry } else { None };
run_with_retry(pool, effective, |conn| async move {
let result = conn
.execute_query(
query_id.to_string(),
query.to_string(),
settings.to_vec(),
params.to_vec(),
)
.await;
if let Err(ref e) = result {
poison_unless_server_rejected(&conn, e);
}
result
})
.await
}
pub async fn execute_stream_via_pool(
pool: &Arc<NativePool>,
query_id: &str,
query: &str,
settings: &[(String, String)],
params: &[(String, String)],
retry: Option<RetryPolicy>,
) -> Result<TcpRawCursor> {
run_with_retry(pool, retry, |conn| {
let query_id = query_id.to_string();
let query = query.to_string();
let settings = settings.to_vec();
let params = params.to_vec();
async move {
match conn
.execute_stream_cursor(query_id, query, settings, params)
.await
{
Ok(mut cursor) => {
cursor.hold_pool_slot(conn);
Ok(cursor)
}
Err(e) => {
poison_unless_server_rejected(&conn, &e);
Err(e)
}
}
}
})
.await
}
fn poison_unless_server_rejected(conn: &ConnectionHandle, e: &Error) {
if !matches!(e, Error::ServerException { .. }) {
conn.poison();
}
}
#[non_exhaustive]
pub struct TcpInsertSession {
handle: deadpool::managed::Object<crate::tcp::pool::TcpConnectionManager>,
pub server_columns: Vec<(String, String)>,
pub server_revision: u64,
}
impl TcpInsertSession {
pub async fn send_block(
&self,
column_bytes: Vec<u8>,
num_columns: u64,
num_rows: u64,
) -> Result<()> {
let result = self
.handle
.send_insert_block(column_bytes, num_columns, num_rows)
.await;
if let Err(ref e) = result {
poison_unless_server_rejected(&self.handle, e);
}
result
}
pub async fn finish(self) -> Result<()> {
let result = self.handle.finish_insert().await;
if let Err(ref e) = result {
poison_unless_server_rejected(&self.handle, e);
}
result
}
pub fn abort(self) {
self.handle.poison();
}
}
pub async fn insert_native_via_pool(
pool: &Arc<NativePool>,
query_id: &str,
sql: &str,
settings: &[(String, String)],
) -> Result<TcpInsertSession> {
let conn = pool.get().await.map_err(map_pool_error)?;
let server_columns = match conn
.begin_insert(
query_id.to_string(),
sql.to_string(),
settings.to_vec(),
Vec::new(),
)
.await
{
Ok(cols) => cols,
Err(e) => {
poison_unless_server_rejected(&conn, &e);
return Err(e);
}
};
let server_revision = conn.server_hello().revision;
Ok(TcpInsertSession {
handle: conn,
server_columns,
server_revision,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use crate::native::io::ClickHouseWrite;
use crate::tcp::handshake::HandshakeConfig;
use crate::tcp::mock::{serve_one_handshake, write_schema_block, write_uint64_payload_block};
use crate::tcp::pool::{ConnectKindConfig, PoolConfig, build_pool};
use crate::tcp::protocol::ServerPacketId;
use crate::tcp::retry::is_retriable_transport;
fn pool_of(addr: String, max_size: usize) -> Arc<NativePool> {
Arc::new(
build_pool(
vec![addr],
ConnectKindConfig::Plain,
HandshakeConfig::default(),
PoolConfig {
max_size,
..PoolConfig::default()
},
#[cfg(feature = "tls")]
crate::tcp::pool::TcpTls::NotConfigured,
)
.expect("test pool builds"),
)
}
async fn write_exception(sock: &mut TcpStream, code: i32, message: &str) {
sock.write_var_uint(ServerPacketId::Exception as u64)
.await
.unwrap();
sock.write_i32_le(code).await.unwrap();
sock.write_string(b"DB::Exception").await.unwrap();
sock.write_string(message.as_bytes()).await.unwrap();
sock.write_string(b"").await.unwrap();
AsyncWriteExt::write_u8(sock, 0).await.unwrap();
sock.flush().await.unwrap();
}
#[tokio::test]
async fn execute_query_server_exception_does_not_poison_the_connection() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
serve_one_handshake(&mut sock).await;
write_exception(&mut sock, 60, "no such table").await;
tokio::time::sleep(Duration::from_millis(300)).await;
sock
});
let pool = pool_of(addr, 1);
let err = execute_query_via_pool(&pool, "q", "SELECT 1", &[], &[], None, false)
.await
.expect_err("the server rejected the statement");
assert!(matches!(err, Error::ServerException { code: 60, .. }));
let conn = pool.get().await.expect("acquire after a rejection");
assert!(
conn.is_alive(),
"a server rejection must not poison the connection"
);
drop(conn);
let _sock = server.await.unwrap();
}
#[tokio::test]
async fn insert_native_pool_error_stays_typed() {
let dead = TcpListener::bind("127.0.0.1:0").await.unwrap();
let refusing = dead.local_addr().unwrap().to_string();
drop(dead);
let pool = pool_of(refusing, 1);
let Err(err) = insert_native_via_pool(&pool, "q", "INSERT INTO t FORMAT Native", &[]).await
else {
panic!("every endpoint refuses; the insert must not open")
};
assert!(
!matches!(err, Error::Custom(_)),
"a backend failure must stay typed, got {err:?}"
);
assert!(
is_retriable_transport(&err),
"a refused connect must classify as retriable, got {err:?}"
);
}
#[tokio::test]
async fn streaming_select_holds_its_pool_slot_until_the_cursor_drops() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
serve_one_handshake(&mut sock).await;
write_schema_block(&mut sock, &[("n", "UInt64")]).await;
write_uint64_payload_block(&mut sock, &[1, 2, 3]).await;
sock.write_var_uint(ServerPacketId::EndOfStream as u64)
.await
.unwrap();
sock.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
sock
});
let pool = pool_of(addr, 1);
let cursor = execute_stream_via_pool(&pool, "q", "SELECT number AS n", &[], &[], None)
.await
.expect("stream opens");
let blocked = tokio::time::timeout(Duration::from_millis(150), pool.get()).await;
assert!(
blocked.is_err(),
"the cursor must hold its pool slot for its whole life"
);
drop(cursor);
let freed = tokio::time::timeout(Duration::from_secs(2), pool.get())
.await
.expect("the slot must free once the cursor drops");
assert!(freed.is_ok(), "re-acquire after a cursor drop");
let _sock = server.await.unwrap();
}
}