use super::{NodeAddress, TlsMode};
use async_trait::async_trait;
use futures_intrusive::sync::ManualResetEvent;
use logger_core::{log_debug, log_error, log_trace, log_warn};
use redis::aio::{DisconnectNotifier, MultiplexedConnection};
use redis::{
AddressResolver, GlideConnectionOptions, PushInfo, RedisConnectionInfo, RedisError,
RedisResult, RetryStrategy,
};
use std::fmt;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{RwLock, RwLockReadGuard};
use std::time::Duration;
use telemetrylib::Telemetry;
use tokio::sync::{Notify, mpsc};
use tokio::task;
use tokio::time::timeout;
use tokio_retry2::{Retry, RetryError};
use super::{run_with_timeout, types::DEFAULT_CONNECTION_TIMEOUT};
const WRITE_LOCK_ERR: &str = "Failed to acquire the write lock";
const READ_LOCK_ERR: &str = "Failed to acquire the read lock";
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum ReconnectReason {
ConnectionDropped,
CreateError,
}
#[derive(Clone)]
pub struct IAMTokenHandle {
pub(crate) cached_token: Arc<tokio::sync::RwLock<String>>,
pub(crate) token_created_at: Arc<tokio::sync::RwLock<tokio::time::Instant>>,
pub(crate) iam_token_state: crate::iam::IamTokenState,
}
impl IAMTokenHandle {
pub(crate) async fn get_valid_token_inner(&self) -> Option<String> {
use crate::iam::TOKEN_TTL_SECONDS;
let is_expired = {
let ts = self.token_created_at.read().await;
ts.elapsed() >= std::time::Duration::from_secs(TOKEN_TTL_SECONDS)
};
if is_expired {
logger_core::log_info(
"IAM reconnect",
"Token expired, generating a fresh token before reconnection",
);
match crate::iam::IAMTokenManager::generate_token_with_backoff(&self.iam_token_state)
.await
{
Ok(new_token) => {
{
let mut guard = self.cached_token.write().await;
*guard = new_token.clone();
}
{
let mut ts = self.token_created_at.write().await;
*ts = tokio::time::Instant::now();
}
return Some(new_token);
}
Err(err) => {
logger_core::log_error(
"IAM reconnect",
format!("Failed to generate fresh IAM token, using cached token: {err}"),
);
}
}
}
let guard = self.cached_token.read().await;
let token = guard.clone();
if token.is_empty() { None } else { Some(token) }
}
}
#[async_trait::async_trait]
impl redis::IAMTokenProvider for IAMTokenHandle {
async fn get_valid_token(&self) -> Option<String> {
self.get_valid_token_inner().await
}
}
struct ConnectionBackend {
connection_available_signal: ManualResetEvent,
connection_info: RwLock<redis::Client>,
client_dropped_flagged: AtomicBool,
iam_token_handle: Option<IAMTokenHandle>,
}
enum ConnectionState {
Connected(MultiplexedConnection),
Reconnecting,
InitializedDisconnected,
}
struct InnerReconnectingConnection {
state: Mutex<ConnectionState>,
backend: ConnectionBackend,
}
#[derive(Clone)]
pub(super) struct ReconnectingConnection {
inner: Arc<InnerReconnectingConnection>,
connection_options: GlideConnectionOptions,
}
impl fmt::Debug for ReconnectingConnection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.node_address())
}
}
async fn get_multiplexed_connection(
client: &redis::Client,
connection_options: &GlideConnectionOptions,
) -> RedisResult<MultiplexedConnection> {
run_with_timeout(
Some(
connection_options
.connection_timeout
.unwrap_or(DEFAULT_CONNECTION_TIMEOUT),
),
client.get_multiplexed_async_connection(connection_options.clone()),
)
.await
}
#[derive(Clone)]
struct TokioDisconnectNotifier {
disconnect_notifier: Arc<Notify>,
}
#[async_trait]
impl DisconnectNotifier for TokioDisconnectNotifier {
fn notify_disconnect(&mut self) {
self.disconnect_notifier.notify_one();
}
async fn wait_for_disconnect_with_timeout(&self, max_wait: &Duration) {
let _ = timeout(*max_wait, async {
self.disconnect_notifier.notified().await;
})
.await;
}
fn clone_box(&self) -> Box<dyn DisconnectNotifier> {
Box::new(self.clone())
}
}
impl TokioDisconnectNotifier {
fn new() -> TokioDisconnectNotifier {
TokioDisconnectNotifier {
disconnect_notifier: Arc::new(Notify::new()),
}
}
}
#[allow(clippy::result_large_err)]
async fn create_connection(
connection_backend: ConnectionBackend,
retry_strategy: RetryStrategy,
push_sender: Option<mpsc::UnboundedSender<PushInfo>>,
discover_az: bool,
connection_timeout: Duration,
tcp_nodelay: bool,
pubsub_synchronizer: Option<Arc<dyn crate::pubsub::PubSubSynchronizer>>,
) -> Result<ReconnectingConnection, Box<(ReconnectingConnection, RedisError)>> {
let client = {
let guard = connection_backend
.connection_info
.read()
.expect(READ_LOCK_ERR);
guard.clone()
};
let connection_options = GlideConnectionOptions {
push_sender,
disconnect_notifier: Some::<Box<dyn DisconnectNotifier>>(Box::new(
TokioDisconnectNotifier::new(),
)),
discover_az,
connection_timeout: Some(connection_timeout),
connection_retry_strategy: Some(retry_strategy),
tcp_nodelay,
pubsub_synchronizer,
iam_token_provider: None,
};
let action = || async {
client
.get_multiplexed_async_connection(connection_options.clone())
.await
.map_err(|e| {
let is_permanent = matches!(
e.kind(),
redis::ErrorKind::AuthenticationFailed
| redis::ErrorKind::InvalidClientConfig
| redis::ErrorKind::RESP3NotSupported
) || e.to_string().contains("NOAUTH")
|| e.to_string().contains("WRONGPASS");
if is_permanent {
RetryError::permanent(e)
} else {
RetryError::transient(e)
}
})
};
let retry_future = Retry::spawn(retry_strategy.get_bounded_backoff_dur_iterator(), action);
let result = timeout(connection_timeout, retry_future).await;
match result {
Ok(Ok(connection)) => {
log_debug(
"connection creation",
format!(
"Connection to {} created",
connection_backend
.get_backend_client()
.get_connection_info()
.addr
),
);
Telemetry::incr_total_connections(1);
Ok(ReconnectingConnection {
inner: Arc::new(InnerReconnectingConnection {
state: Mutex::new(ConnectionState::Connected(connection)),
backend: connection_backend,
}),
connection_options,
})
}
err => {
let err: RedisError = match err {
Ok(Err(e)) => e,
_ => std::io::Error::from(std::io::ErrorKind::TimedOut).into(),
};
log_warn(
"connection creation",
format!(
"Failed connecting to {}, due to {err}",
connection_backend
.get_backend_client()
.get_connection_info()
.addr
),
);
let connection = ReconnectingConnection {
inner: Arc::new(InnerReconnectingConnection {
state: Mutex::new(ConnectionState::InitializedDisconnected),
backend: connection_backend,
}),
connection_options,
};
connection.reconnect(ReconnectReason::CreateError);
Err(Box::new((connection, err)))
}
}
}
fn get_client(
address: &NodeAddress,
tls_mode: TlsMode,
redis_connection_info: redis::RedisConnectionInfo,
tls_params: Option<redis::TlsConnParams>,
address_resolver: Option<&std::sync::Arc<dyn super::AddressResolver>>,
) -> redis::Client {
let connection_info = super::get_connection_info(
address,
tls_mode,
redis_connection_info,
tls_params,
address_resolver,
);
redis::Client::open(connection_info).unwrap() }
impl ConnectionBackend {
fn get_backend_client(&self) -> RwLockReadGuard<'_, redis::Client> {
self.connection_info.read().expect(READ_LOCK_ERR)
}
}
impl ReconnectingConnection {
#[allow(clippy::result_large_err)]
#[allow(clippy::too_many_arguments)]
pub(super) async fn new(
address: &NodeAddress,
connection_retry_strategy: RetryStrategy,
redis_connection_info: RedisConnectionInfo,
tls_mode: TlsMode,
push_sender: Option<mpsc::UnboundedSender<PushInfo>>,
discover_az: bool,
connection_timeout: Duration,
tls_params: Option<redis::TlsConnParams>,
tcp_nodelay: bool,
pubsub_synchronizer: Option<Arc<dyn crate::pubsub::PubSubSynchronizer>>,
address_resolver: Option<&std::sync::Arc<dyn AddressResolver>>,
iam_token_handle: Option<IAMTokenHandle>,
) -> Result<ReconnectingConnection, Box<(ReconnectingConnection, RedisError)>> {
log_debug(
"connection creation",
format!("Attempting connection to {address}"),
);
let connection_info = get_client(
address,
tls_mode,
redis_connection_info,
tls_params,
address_resolver,
);
let backend = ConnectionBackend {
connection_info: RwLock::new(connection_info),
connection_available_signal: ManualResetEvent::new(true),
client_dropped_flagged: AtomicBool::new(false),
iam_token_handle,
};
create_connection(
backend,
connection_retry_strategy,
push_sender,
discover_az,
connection_timeout,
tcp_nodelay,
pubsub_synchronizer,
)
.await
}
pub(crate) fn node_address(&self) -> String {
self.inner
.backend
.get_backend_client()
.get_connection_info()
.addr
.to_string()
}
pub(super) fn is_dropped(&self) -> bool {
self.inner
.backend
.client_dropped_flagged
.load(Ordering::Relaxed)
}
pub(super) fn mark_as_dropped(&self) {
Telemetry::decr_total_connections(1);
self.inner
.backend
.client_dropped_flagged
.store(true, Ordering::Relaxed)
}
pub(super) async fn try_get_connection(&self) -> Option<MultiplexedConnection> {
let guard = self.inner.state.lock().unwrap();
if let ConnectionState::Connected(connection) = &*guard {
Some(connection.clone())
} else {
None
}
}
pub(super) async fn get_connection(&self) -> Result<MultiplexedConnection, RedisError> {
loop {
self.inner.backend.connection_available_signal.wait().await;
if let Some(connection) = self.try_get_connection().await {
return Ok(connection);
}
}
}
pub(super) fn reconnect(&self, reason: ReconnectReason) {
{
let mut guard = self.inner.state.lock().unwrap();
if matches!(*guard, ConnectionState::Reconnecting) {
log_trace("reconnect", "already started");
return;
}
self.inner.backend.connection_available_signal.reset();
*guard = ConnectionState::Reconnecting;
};
log_debug("reconnect", "starting");
let connection_clone = self.clone();
if reason.eq(&ReconnectReason::ConnectionDropped) {
Telemetry::decr_total_connections(1);
}
task::spawn(async move {
let has_iam = connection_clone.inner.backend.iam_token_handle.is_some();
let static_client = if !has_iam {
Some({
let guard = connection_clone.inner.backend.get_backend_client();
guard.clone()
})
} else {
None
};
let infinite_backoff_dur_iterator = connection_clone
.connection_options
.connection_retry_strategy
.unwrap()
.get_infinite_backoff_dur_iterator();
for sleep_duration in infinite_backoff_dur_iterator {
if connection_clone.is_dropped() {
log_debug(
"ReconnectingConnection",
"reconnect stopped after client was dropped",
);
return;
}
if let Some(handle) = &connection_clone.inner.backend.iam_token_handle
&& let Some(valid_token) = handle.get_valid_token_inner().await
{
let mut client = connection_clone
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_password(Some(valid_token));
log_debug(
"reconnect",
"Updated connection password with valid IAM token before reconnection attempt",
);
}
let client = if let Some(ref c) = static_client {
c.clone()
} else {
let guard = connection_clone.inner.backend.get_backend_client();
guard.clone()
};
match get_multiplexed_connection(&client, &connection_clone.connection_options)
.await
{
Ok(mut connection) => {
if connection
.send_packed_command(&redis::cmd("PING"))
.await
.is_err()
{
tokio::time::sleep(sleep_duration).await;
continue;
}
{
let mut guard = connection_clone.inner.state.lock().unwrap();
log_debug("reconnect", "completed successfully");
connection_clone
.inner
.backend
.connection_available_signal
.set();
*guard = ConnectionState::Connected(connection);
}
Telemetry::incr_total_connections(1);
return;
}
Err(_) => tokio::time::sleep(sleep_duration).await,
}
}
});
}
pub fn is_connected(&self) -> bool {
!matches!(
*self.inner.state.lock().unwrap(),
ConnectionState::Reconnecting
)
}
pub async fn wait_for_disconnect_with_timeout(&self, max_wait: &Duration) {
if let Some(disconnect_notifier) = &self.connection_options.disconnect_notifier {
disconnect_notifier
.wait_for_disconnect_with_timeout(max_wait)
.await;
} else {
log_error("disconnect notifier", "BUG! Disconnect notifier is not set");
}
}
pub(crate) fn update_connection_password(&self, new_password: Option<String>) {
let mut client = self
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_password(new_password);
}
pub(crate) fn update_connection_database(&self, new_database_id: i64) {
let mut client = self
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_database(new_database_id);
}
pub(crate) fn update_connection_client_name(&self, new_client_name: Option<String>) {
let mut client = self
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_client_name(new_client_name);
}
pub(crate) fn update_connection_username(&self, new_username: Option<String>) {
let mut client = self
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_username(new_username);
}
pub(crate) fn update_connection_protocol(&self, new_protocol: redis::ProtocolVersion) {
let mut client = self
.inner
.backend
.connection_info
.write()
.expect(WRITE_LOCK_ERR);
client.update_protocol(new_protocol);
}
pub(crate) fn get_username(&self) -> Option<String> {
let client = self.inner.backend.get_backend_client();
client.get_connection_info().redis.username.clone()
}
}