use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use crate::error::{Error, ErrorKind, Result};
use crate::frame::{self, Frame, Tag};
use crate::options::Options;
use crate::response::{decode_variant, Response};
use crate::transport::{self, Transport};
const PROTOCOL_NAME: &str = "tricore";
const PROTOCOL_VERSION: u32 = 1;
const CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
pub struct Client {
transport: Transport,
database: String,
session_id: Option<String>,
granted_features: u64,
txn_open: bool,
request_counter: u64,
request_id_prefix: String,
last_request_id: Option<String>,
request_timeout_ms: Option<u64>,
read_timeout: Option<Duration>,
fault: Option<Error>,
closed: bool,
}
#[derive(Deserialize)]
struct HelloOk {
#[serde(default)]
ok: bool,
#[serde(default)]
message: String,
#[serde(default)]
features: u64,
}
#[derive(Deserialize)]
struct AuthOk {
#[serde(default)]
ok: bool,
#[serde(default)]
session_id: Option<String>,
#[serde(default)]
message: String,
}
#[derive(Deserialize)]
struct WireResponse {
#[serde(default)]
request_id: String,
#[serde(default)]
status: String,
#[serde(default)]
data: Value,
#[serde(default)]
diagnostics: Option<Diagnostics>,
}
#[derive(Deserialize, Default)]
struct Diagnostics {
#[serde(default)]
route: Option<String>,
#[serde(default)]
elapsed_ms: Option<i64>,
#[serde(default)]
warnings: Vec<String>,
#[serde(default)]
error_code: Option<String>,
#[serde(default)]
leader_hint: Option<String>,
}
impl Client {
pub fn connect(options: &Options) -> Result<Client> {
let tcp = transport::dial(&options.host, options.port, options.connect_timeout)?;
let transport = match &options.tls {
#[cfg(feature = "tls")]
Some(tls) => {
let stream = Transport::Tcp(tcp);
stream.set_read_timeout(options.connect_timeout)?;
stream.set_write_timeout(options.connect_timeout)?;
let Transport::Tcp(tcp) = stream else {
unreachable!("just constructed as Tcp")
};
crate::tls::wrap(tcp, tls)?
}
#[cfg(not(feature = "tls"))]
Some(_) => {
return Err(Error::invalid(
"this build has no TLS support: enable the crate's `tls` feature",
))
}
None => Transport::Tcp(tcp),
};
let mut client = Client {
transport,
database: options.database.clone(),
session_id: None,
granted_features: 0,
txn_open: false,
request_counter: 0,
request_id_prefix: next_connection_prefix(),
last_request_id: None,
request_timeout_ms: None,
read_timeout: None,
fault: None,
closed: false,
};
client.transport.set_read_timeout(options.connect_timeout)?;
client
.transport
.set_write_timeout(options.connect_timeout)?;
client.hello(&options.client_name, options.announced_features())?;
if let Some(user) = &options.user {
let secret = options.secret.clone().unwrap_or_default();
client.auth(user, &secret)?;
}
client.transport.set_write_timeout(None)?;
client.set_read_timeout(options.read_timeout)?;
Ok(client)
}
fn hello(&mut self, client_name: &str, features: u64) -> Result<()> {
let payload = json!({
"protocol": PROTOCOL_NAME,
"version": {"major": PROTOCOL_VERSION, "minor": 0},
"client": client_name,
"features": features,
});
let frame = self.exchange(Tag::Hello, Some(&payload))?;
match frame.tag {
Tag::HelloOk => {}
Tag::Error => {
return Err(Error::new(ErrorKind::Handshake, error_text(&frame.payload))
.with_code_opt(error_code(&frame.payload)))
}
other => {
return Err(
self.poison(Error::protocol(format!("expected HELLO_OK, got {other:?}")))
)
}
}
let body: HelloOk = parse_body(&frame.payload, "HELLO_OK")?;
if !body.ok {
let message = if body.message.is_empty() {
"the server refused the handshake".to_string()
} else {
body.message
};
return Err(Error::new(ErrorKind::Handshake, message));
}
self.granted_features = body.features;
Ok(())
}
fn auth(&mut self, user: &str, secret: &str) -> Result<()> {
let payload = json!({
"username": user,
"secret": crate::wire::byte_list(secret.as_bytes()),
});
let frame = self.exchange(Tag::Auth, Some(&payload))?;
match frame.tag {
Tag::AuthOk => {}
Tag::Error => {
return Err(Error::new(ErrorKind::Auth, error_text(&frame.payload))
.with_code_opt(error_code(&frame.payload)))
}
other => {
return Err(self.poison(Error::protocol(format!("expected AUTH_OK, got {other:?}"))))
}
}
let body: AuthOk = parse_body(&frame.payload, "AUTH_OK")?;
if !body.ok {
let message = if body.message.is_empty() {
"authentication refused".to_string()
} else {
body.message
};
return Err(Error::new(ErrorKind::Auth, message));
}
self.session_id = body.session_id;
Ok(())
}
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub fn granted_features(&self) -> u64 {
self.granted_features
}
pub fn server_params_granted(&self) -> bool {
self.granted_features & crate::options::FEATURE_SERVER_PARAMS != 0
}
pub fn session_txn_granted(&self) -> bool {
self.granted_features & crate::options::FEATURE_SESSION_TXN != 0
}
pub fn in_transaction(&self) -> bool {
self.txn_open && self.fault.is_none() && !self.closed
}
pub(crate) fn set_txn_open(&mut self, open: bool) {
self.txn_open = open;
}
pub fn database(&self) -> &str {
&self.database
}
pub fn is_poisoned(&self) -> bool {
self.fault.is_some()
}
pub fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
self.read_timeout = timeout.filter(|d| !d.is_zero());
self.transport.set_read_timeout(self.read_timeout)
}
pub fn set_request_timeout(&mut self, timeout: Option<Duration>) {
self.request_timeout_ms = timeout
.filter(|d| !d.is_zero())
.map(|d| d.as_millis().min(u64::MAX as u128) as u64);
}
pub fn last_request_id(&self) -> Option<&str> {
self.last_request_id.as_deref()
}
pub fn ping(&mut self) -> Result<()> {
let frame = self.exchange(Tag::Ping, None)?;
match frame.tag {
Tag::Pong => Ok(()),
other => Err(self.poison(Error::protocol(format!("expected PONG, got {other:?}")))),
}
}
pub fn cancel(&mut self, request_id: &str) -> Result<u64> {
if request_id.is_empty() {
return Err(Error::invalid("a request id to cancel must not be empty"));
}
let frame = self.exchange(Tag::Cancel, Some(&json!({"request_id": request_id})))?;
match frame.tag {
Tag::CancelOk => {}
Tag::Error => {
return Err(Error::new(ErrorKind::Server, error_text(&frame.payload))
.with_code_opt(error_code(&frame.payload)))
}
other => {
return Err(self.poison(Error::protocol(format!(
"expected CANCEL_OK, got {other:?}"
))))
}
}
#[derive(Deserialize)]
struct CancelOk {
#[serde(default)]
cancelled: u64,
}
let body: CancelOk = parse_body(&frame.payload, "CANCEL_OK")?;
Ok(body.cancelled)
}
pub fn request(&mut self, op: impl Into<Value>) -> Result<Response> {
self.send(op.into())
}
pub(crate) fn send(&mut self, op: Value) -> Result<Response> {
self.request_counter += 1;
let request_id = format!("rs-{}-{}", self.request_id_prefix, self.request_counter);
self.last_request_id = Some(request_id.clone());
let mut envelope = Map::new();
envelope.insert("request_id".into(), Value::String(request_id));
envelope.insert("database".into(), Value::String(self.database.clone()));
envelope.insert("op".into(), op);
if let Some(ms) = self.request_timeout_ms {
envelope.insert("options".into(), json!({"timeout_ms": ms}));
}
let frame = self.exchange(Tag::Request, Some(&Value::Object(envelope)))?;
match frame.tag {
Tag::Response => {}
Tag::Error => {
return Err(Error::new(ErrorKind::Refused, error_text(&frame.payload))
.with_code_opt(error_code(&frame.payload)))
}
other => {
return Err(
self.poison(Error::protocol(format!("expected RESPONSE, got {other:?}")))
)
}
}
let wire: WireResponse = parse_body(&frame.payload, "RESPONSE")?;
let diagnostics = wire.diagnostics.unwrap_or_default();
let (kind, data) = decode_variant(wire.data);
let response = Response {
request_id: wire.request_id,
status: wire.status,
kind,
data,
route: diagnostics.route,
elapsed_ms: diagnostics.elapsed_ms,
warnings: diagnostics.warnings,
error_code: diagnostics.error_code,
leader_hint: diagnostics.leader_hint,
};
if response.status != "ok" {
let mut error = Error {
kind: ErrorKind::Server,
code: response.error_code.clone(),
message: response.failure_message(),
leader_hint: response.leader_hint.clone(),
};
if error.code.is_none() {
error.code = None;
}
return Err(error);
}
Ok(response)
}
fn exchange(&mut self, tag: Tag, payload: Option<&Value>) -> Result<Frame> {
if let Some(fault) = &self.fault {
return Err(Error::new(
ErrorKind::Closed,
format!(
"this connection was dropped after an earlier failure and cannot be reused: {}",
fault.message
),
));
}
if self.closed {
return Err(Error::new(ErrorKind::Closed, "this connection is closed"));
}
let body = match payload {
Some(value) => serde_json::to_vec(value)
.map_err(|e| Error::invalid(format!("cannot encode the request: {e}")))?,
None => Vec::new(),
};
frame::encode(tag, &body)?;
if let Err(e) = frame::write_frame(&mut self.transport, tag, &body) {
return Err(self.poison(e));
}
match frame::read_frame(&mut self.transport) {
Ok(frame) => Ok(frame),
Err(e) => Err(self.poison(e)),
}
}
fn poison(&mut self, error: Error) -> Error {
if self.fault.is_none() {
self.transport.shutdown();
self.txn_open = false;
self.fault = Some(error.clone());
}
error
}
pub fn close(mut self) -> Result<()> {
self.close_inner()
}
fn close_inner(&mut self) -> Result<()> {
if self.closed {
return Ok(());
}
self.closed = true;
self.txn_open = false;
if self.fault.is_some() {
self.transport.shutdown();
return Ok(());
}
let _ = self.transport.set_read_timeout(Some(CLOSE_TIMEOUT));
let _ = self.transport.set_write_timeout(Some(CLOSE_TIMEOUT));
let _ = frame::write_frame(&mut self.transport, Tag::Close, &[]);
let _ = frame::read_frame(&mut self.transport);
self.transport.shutdown();
Ok(())
}
}
impl Drop for Client {
fn drop(&mut self) {
let _ = self.close_inner();
}
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("database", &self.database)
.field("session_id", &self.session_id)
.field("granted_features", &self.granted_features)
.field("in_transaction", &self.in_transaction())
.field("poisoned", &self.is_poisoned())
.finish()
}
}
fn parse_body<T: serde::de::DeserializeOwned>(payload: &[u8], what: &str) -> Result<T> {
serde_json::from_slice(payload)
.map_err(|e| Error::protocol(format!("malformed {what} body: {e}")))
}
fn error_text(payload: &[u8]) -> String {
if payload.is_empty() {
return "the server refused the connection without saying why".to_string();
}
let Ok(value) = serde_json::from_slice::<Value>(payload) else {
return String::from_utf8_lossy(payload).into_owned();
};
for key in ["message", "error"] {
if let Some(text) = value.get(key).and_then(Value::as_str) {
if !text.is_empty() {
return text.to_string();
}
}
}
value.to_string()
}
fn error_code(payload: &[u8]) -> Option<String> {
let value: Value = serde_json::from_slice(payload).ok()?;
value
.get("code")
.and_then(Value::as_str)
.filter(|c| !c.is_empty())
.map(str::to_string)
}
fn next_connection_prefix() -> String {
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
format!(
"{:x}{:x}{:x}",
std::process::id(),
stamp,
SEQUENCE.fetch_add(1, Ordering::Relaxed)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_connection_prefix_is_unique_per_connection() {
let a = next_connection_prefix();
let b = next_connection_prefix();
assert_ne!(a, b);
}
#[test]
fn error_frames_are_read_for_message_and_code() {
assert_eq!(error_text(br#"{"message":"nope"}"#), "nope");
assert_eq!(error_text(br#"{"error":"nope"}"#), "nope");
assert_eq!(
error_code(br#"{"message":"x","code":"frame_too_large"}"#),
Some("frame_too_large".to_string())
);
assert_eq!(error_code(br#"{"message":"x"}"#), None);
assert!(!error_text(b"").is_empty());
}
}