blob: 9ce909cceba399ca52781a0fba38165a793577a8 [file]
use crate::binary::binary_client::BinaryClient;
use crate::binary::{BinaryTransport, ClientState};
use crate::client::{
AutoLogin, Client, ConnectionString, Credentials, PersonalAccessTokenClient, UserClient,
};
use crate::command::Command;
use crate::diagnostic::DiagnosticEvent;
use crate::error::{IggyError, IggyErrorDiscriminants};
use crate::tcp::config::TcpClientConfig;
use crate::utils::duration::IggyDuration;
use crate::utils::timestamp::IggyTimestamp;
use async_broadcast::{broadcast, Receiver, Sender};
use async_trait::async_trait;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio::time::sleep;
use tokio_native_tls::native_tls::TlsConnector;
use tokio_native_tls::TlsStream;
use tracing::{error, info, trace, warn};
const REQUEST_INITIAL_BYTES_LENGTH: usize = 4;
const RESPONSE_INITIAL_BYTES_LENGTH: usize = 8;
const NAME: &str = "Iggy";
/// TCP client for interacting with the Iggy API.
/// It requires a valid server address.
#[derive(Debug)]
pub struct TcpClient {
pub(crate) stream: Mutex<Option<Box<dyn ConnectionStream>>>,
pub(crate) config: Arc<TcpClientConfig>,
pub(crate) state: Mutex<ClientState>,
events: (Sender<DiagnosticEvent>, Receiver<DiagnosticEvent>),
connected_at: Mutex<Option<IggyTimestamp>>,
}
unsafe impl Send for TcpClient {}
unsafe impl Sync for TcpClient {}
#[async_trait]
pub(crate) trait ConnectionStream: Debug + Sync + Send {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, IggyError>;
async fn write(&mut self, buf: &[u8]) -> Result<(), IggyError>;
async fn flush(&mut self) -> Result<(), IggyError>;
}
#[derive(Debug)]
struct TcpConnectionStream {
reader: BufReader<OwnedReadHalf>,
writer: BufWriter<OwnedWriteHalf>,
}
impl TcpConnectionStream {
pub fn new(stream: TcpStream) -> Self {
let (reader, writer) = stream.into_split();
Self {
reader: BufReader::new(reader),
writer: BufWriter::new(writer),
}
}
}
#[derive(Debug)]
pub(crate) struct TcpTlsConnectionStream {
stream: TlsStream<TcpStream>,
}
unsafe impl Send for TcpConnectionStream {}
unsafe impl Sync for TcpConnectionStream {}
unsafe impl Send for TcpTlsConnectionStream {}
unsafe impl Sync for TcpTlsConnectionStream {}
#[async_trait]
impl ConnectionStream for TcpConnectionStream {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, IggyError> {
self.reader.read_exact(buf).await.map_err(|error| {
error!("Failed to read data from the TCP connection: {error}");
IggyError::from(error)
})
}
async fn write(&mut self, buf: &[u8]) -> Result<(), IggyError> {
Ok(self.writer.write_all(buf).await?)
}
async fn flush(&mut self) -> Result<(), IggyError> {
Ok(self.writer.flush().await?)
}
}
#[async_trait]
impl ConnectionStream for TcpTlsConnectionStream {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, IggyError> {
self.stream.read_exact(buf).await.map_err(|error| {
error!("Failed to read data from the TCP connection: {error}");
IggyError::from(error)
})
}
async fn write(&mut self, buf: &[u8]) -> Result<(), IggyError> {
Ok(self.stream.write_all(buf).await?)
}
async fn flush(&mut self) -> Result<(), IggyError> {
Ok(())
}
}
impl Default for TcpClient {
fn default() -> Self {
TcpClient::create(Arc::new(TcpClientConfig::default())).unwrap()
}
}
#[async_trait]
impl Client for TcpClient {
async fn connect(&self) -> Result<(), IggyError> {
TcpClient::connect(self).await
}
async fn disconnect(&self) -> Result<(), IggyError> {
TcpClient::disconnect(self).await
}
async fn subscribe_events(&self) -> Receiver<DiagnosticEvent> {
self.events.1.clone()
}
}
#[async_trait]
impl BinaryTransport for TcpClient {
async fn get_state(&self) -> ClientState {
*self.state.lock().await
}
async fn set_state(&self, state: ClientState) {
*self.state.lock().await = state;
}
async fn send_with_response<T: Command>(&self, command: &T) -> Result<Bytes, IggyError> {
command.validate()?;
self.send_raw_with_response(command.code(), command.to_bytes())
.await
}
async fn send_raw_with_response(&self, code: u32, payload: Bytes) -> Result<Bytes, IggyError> {
let result = self.send_raw(code, payload.clone()).await;
if result.is_ok() {
return result;
}
let error = result.unwrap_err();
if !matches!(
error,
IggyError::Disconnected | IggyError::EmptyResponse | IggyError::Unauthenticated
) {
return Err(error);
}
if !self.config.reconnection.enabled {
return Err(IggyError::Disconnected);
}
self.disconnect().await?;
info!("Reconnecting to the server...");
self.connect().await?;
self.send_raw(code, payload).await
}
async fn publish_event(&self, event: DiagnosticEvent) {
if let Err(error) = self.events.0.broadcast(event).await {
error!("Failed to send a TCP diagnostic event: {error}");
}
}
}
impl BinaryClient for TcpClient {}
impl TcpClient {
/// Create a new TCP client for the provided server address.
pub fn new(server_address: &str, auto_sign_in: AutoLogin) -> Result<Self, IggyError> {
Self::create(Arc::new(TcpClientConfig {
server_address: server_address.to_string(),
auto_login: auto_sign_in,
..Default::default()
}))
}
/// Create a new TCP client for the provided server address using TLS.
pub fn new_tls(
server_address: &str,
domain: &str,
auto_sign_in: AutoLogin,
) -> Result<Self, IggyError> {
Self::create(Arc::new(TcpClientConfig {
server_address: server_address.to_string(),
tls_enabled: true,
tls_domain: domain.to_string(),
auto_login: auto_sign_in,
..Default::default()
}))
}
pub fn from_connection_string(connection_string: &str) -> Result<Self, IggyError> {
Self::create(Arc::new(
ConnectionString::from_str(connection_string)?.into(),
))
}
/// Create a new TCP client based on the provided configuration.
pub fn create(config: Arc<TcpClientConfig>) -> Result<Self, IggyError> {
Ok(Self {
config,
stream: Mutex::new(None),
state: Mutex::new(ClientState::Disconnected),
events: broadcast(1000),
connected_at: Mutex::new(None),
})
}
async fn handle_response(
&self,
status: u32,
length: u32,
stream: &mut dyn ConnectionStream,
) -> Result<Bytes, IggyError> {
if status != 0 {
// TEMP: See https://github.com/iggy-rs/iggy/pull/604 for context.
if status == IggyErrorDiscriminants::TopicIdAlreadyExists as u32
|| status == IggyErrorDiscriminants::TopicNameAlreadyExists as u32
|| status == IggyErrorDiscriminants::StreamIdAlreadyExists as u32
|| status == IggyErrorDiscriminants::StreamNameAlreadyExists as u32
|| status == IggyErrorDiscriminants::UserAlreadyExists as u32
|| status == IggyErrorDiscriminants::PersonalAccessTokenAlreadyExists as u32
|| status == IggyErrorDiscriminants::ConsumerGroupIdAlreadyExists as u32
|| status == IggyErrorDiscriminants::ConsumerGroupNameAlreadyExists as u32
{
tracing::debug!(
"Received a server resource already exists response: {} ({})",
status,
IggyError::from_code_as_string(status)
)
} else {
error!(
"Received an invalid response with status: {} ({}).",
status,
IggyError::from_code_as_string(status),
);
}
let mut error_details_buffer = BytesMut::with_capacity(length as usize);
error_details_buffer.put_bytes(0, length as usize);
stream.read(&mut error_details_buffer).await?;
let string_length = error_details_buffer.get_u32_le();
let error_message = String::from_utf8_lossy(&error_details_buffer);
return Err(IggyError::InvalidResponse(
status,
string_length,
error_message.to_string(),
));
}
trace!("Status: OK. Response length: {}", length);
if length <= 1 {
return Ok(Bytes::new());
}
let mut response_buffer = BytesMut::with_capacity(length as usize);
response_buffer.put_bytes(0, length as usize);
stream.read(&mut response_buffer).await?;
Ok(response_buffer.freeze())
}
async fn connect(&self) -> Result<(), IggyError> {
match self.get_state().await {
ClientState::Connected | ClientState::Authenticating | ClientState::Authenticated => {
trace!("Client is already connected.");
return Ok(());
}
ClientState::Connecting => {
trace!("Client is already connecting.");
return Ok(());
}
_ => {}
}
self.set_state(ClientState::Connecting).await;
if let Some(connected_at) = self.connected_at.lock().await.as_ref() {
let now = IggyTimestamp::now();
let elapsed = now.as_micros() - connected_at.as_micros();
let interval = self.config.reconnection.re_establish_after.as_micros();
trace!(
"Elapsed time since last connection: {}",
IggyDuration::from(elapsed)
);
if elapsed < interval {
let remaining = IggyDuration::from(interval - elapsed);
info!("Trying to connect to the server in: {remaining}",);
sleep(remaining.get_duration()).await;
}
}
let tls_enabled = self.config.tls_enabled;
let mut retry_count = 0;
let connection_stream: Box<dyn ConnectionStream>;
let remote_address;
loop {
info!(
"{NAME} client is connecting to server: {}...",
self.config.server_address
);
let connection = TcpStream::connect(&self.config.server_address).await;
if connection.is_err() {
error!(
"Failed to connect to server: {}",
self.config.server_address
);
if !self.config.reconnection.enabled {
warn!("Automatic reconnection is disabled.");
return Err(IggyError::CannotEstablishConnection);
}
let unlimited_retries = self.config.reconnection.max_retries.is_none();
let max_retries = self.config.reconnection.max_retries.unwrap_or_default();
let max_retries_str =
if let Some(max_retries) = self.config.reconnection.max_retries {
max_retries.to_string()
} else {
"unlimited".to_string()
};
let interval_str = self.config.reconnection.interval.as_human_time_string();
if unlimited_retries || retry_count < max_retries {
retry_count += 1;
info!(
"Retrying to connect to server ({retry_count}/{max_retries_str}): {} in: {interval_str}",
self.config.server_address,
);
sleep(self.config.reconnection.interval.get_duration()).await;
continue;
}
self.set_state(ClientState::Disconnected).await;
self.publish_event(DiagnosticEvent::Disconnected).await;
return Err(IggyError::CannotEstablishConnection);
}
let stream = connection.unwrap();
remote_address = stream.peer_addr()?;
if !tls_enabled {
connection_stream = Box::new(TcpConnectionStream::new(stream));
break;
}
let connector = tokio_native_tls::TlsConnector::from(
TlsConnector::builder().build().map_err(|error| {
error!("Failed to create a TLS connector: {error}");
IggyError::CannotEstablishConnection
})?,
);
let stream = tokio_native_tls::TlsConnector::connect(
&connector,
&self.config.tls_domain,
stream,
)
.await
.map_err(|error| {
error!("Failed to establish a TLS connection: {error}");
IggyError::CannotEstablishConnection
})?;
connection_stream = Box::new(TcpTlsConnectionStream { stream });
break;
}
let now = IggyTimestamp::now();
info!("{NAME} client has connected to server: {remote_address} at: {now}",);
self.stream.lock().await.replace(connection_stream);
self.set_state(ClientState::Connected).await;
self.connected_at.lock().await.replace(now);
self.publish_event(DiagnosticEvent::Connected).await;
match &self.config.auto_login {
AutoLogin::Disabled => {
info!("Automatic sign-in is disabled.");
Ok(())
}
AutoLogin::Enabled(credentials) => {
info!("{NAME} client is signing in...");
self.set_state(ClientState::Authenticating).await;
match credentials {
Credentials::UsernamePassword(username, password) => {
self.login_user(username, password).await?;
info!("{NAME} client has signed in with the user credentials, username: {username}",);
Ok(())
}
Credentials::PersonalAccessToken(token) => {
self.login_with_personal_access_token(token).await?;
info!("{NAME} client has signed in with a personal access token.",);
Ok(())
}
}
}
}
}
async fn disconnect(&self) -> Result<(), IggyError> {
if self.get_state().await == ClientState::Disconnected {
return Ok(());
}
info!("{NAME} client is disconnecting from server...");
self.set_state(ClientState::Disconnected).await;
self.stream.lock().await.take();
self.publish_event(DiagnosticEvent::Disconnected).await;
let now = IggyTimestamp::now();
info!("{NAME} client has disconnected from server at: {now}.");
Ok(())
}
async fn send_raw(&self, code: u32, payload: Bytes) -> Result<Bytes, IggyError> {
match self.get_state().await {
ClientState::Disconnected => {
trace!("Cannot send data. Client is not connected.");
return Err(IggyError::NotConnected);
}
ClientState::Connecting => {
trace!("Cannot send data. Client is still connecting.");
return Err(IggyError::NotConnected);
}
_ => {}
}
let mut stream = self.stream.lock().await;
if let Some(stream) = stream.as_mut() {
let payload_length = payload.len() + REQUEST_INITIAL_BYTES_LENGTH;
trace!("Sending a TCP request with code: {code}");
stream.write(&(payload_length as u32).to_le_bytes()).await?;
stream.write(&code.to_le_bytes()).await?;
stream.write(&payload).await?;
stream.flush().await?;
trace!("Sent a TCP request with code: {code}, waiting for a response...");
let mut response_buffer = [0u8; RESPONSE_INITIAL_BYTES_LENGTH];
let read_bytes = stream.read(&mut response_buffer).await.map_err(|error| {
error!(
"Failed to read response for TCP request with code: {code}: {error}",
code = code,
error = error
);
IggyError::Disconnected
})?;
if read_bytes != RESPONSE_INITIAL_BYTES_LENGTH {
error!("Received an invalid or empty response.");
return Err(IggyError::EmptyResponse);
}
let status = u32::from_le_bytes(response_buffer[..4].try_into()?);
let length = u32::from_le_bytes(response_buffer[4..].try_into()?);
return self.handle_response(status, length, stream.as_mut()).await;
}
error!("Cannot send data. Client is not connected.");
Err(IggyError::NotConnected)
}
}