#[cfg(feature = "cluster-async")]
use crate::aio::AsyncPushSender;
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
use crate::auth::StreamingCredentialsProvider;
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
use crate::caching::{CacheConfig, CacheManager};
use crate::client::DEFAULT_CONNECTION_TIMEOUT;
use crate::cluster_handling::NodeAddress;
use crate::cluster_handling::read_routing::{RandomReplicaStrategy, ReadRoutingStrategyFactory};
use crate::connection::{ConnectionAddr, ConnectionInfo, IntoConnectionInfo};
use crate::errors::{ErrorKind, RedisError};
#[cfg(feature = "cluster-async")]
use crate::io::AsyncDNSResolver;
use crate::io::tcp::TcpSettings;
use crate::types::{ProtocolVersion, RedisResult, redacted_if_set, set_or_unset};
use crate::{TlsMode, cluster};
use arcstr::ArcStr;
use rand::RngExt;
use std::collections::HashMap;
#[cfg(feature = "cluster-async")]
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "cluster-async")]
#[derive(Clone, Debug, Default)]
enum OverallResponseTimeout {
#[default]
MatchResponseTimeout,
Explicit(Option<Duration>),
}
use crate::connection::TlsConnParams;
#[cfg(feature = "cluster-async")]
use crate::cluster_async;
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
use crate::tls::{TlsCertificates, retrieve_tls_certificates};
#[derive(Default)]
struct BuilderParams {
password: Option<ArcStr>,
username: Option<ArcStr>,
read_routing_factory: Option<Arc<dyn ReadRoutingStrategyFactory>>,
tls: Option<TlsMode>,
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
certs: Option<TlsCertificates>,
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
danger_accept_invalid_hostnames: bool,
retries_configuration: RetryParams,
connection_timeout: Option<Duration>,
response_timeout: Option<Duration>,
protocol: Option<ProtocolVersion>,
database_id: Option<i64>,
#[cfg(feature = "cluster-async")]
async_push_sender: Option<Arc<dyn AsyncPushSender>>,
pub(crate) tcp_settings: TcpSettings,
#[cfg(feature = "cluster-async")]
async_dns_resolver: Option<Arc<dyn AsyncDNSResolver>>,
#[cfg(feature = "cache-aio")]
cache_config: Option<CacheConfig>,
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
credentials_provider: Option<std::sync::Arc<dyn StreamingCredentialsProvider>>,
#[cfg(feature = "cluster-async")]
overall_response_timeout: OverallResponseTimeout,
#[cfg(feature = "cluster-async")]
connection_concurrency_limit: Option<usize>,
#[cfg(feature = "cluster-async")]
write_backpressure_boundary: Option<usize>,
node_address_map: Option<HashMap<NodeAddress, NodeAddress>>,
#[cfg(feature = "cluster-async")]
max_connection_attempts: Option<NonZeroUsize>,
}
impl std::fmt::Debug for BuilderParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
password,
username,
read_routing_factory,
tls,
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
certs,
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
danger_accept_invalid_hostnames,
retries_configuration,
connection_timeout,
response_timeout,
protocol,
database_id,
#[cfg(feature = "cluster-async")]
async_push_sender,
tcp_settings,
#[cfg(feature = "cluster-async")]
async_dns_resolver,
#[cfg(feature = "cache-aio")]
cache_config,
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
credentials_provider,
#[cfg(feature = "cluster-async")]
overall_response_timeout,
#[cfg(feature = "cluster-async")]
connection_concurrency_limit,
#[cfg(feature = "cluster-async")]
write_backpressure_boundary,
node_address_map,
#[cfg(feature = "cluster-async")]
max_connection_attempts,
} = self;
let mut str = f.debug_struct("BuilderParams");
str.field("password", &redacted_if_set(password))
.field("username", username)
.field("tls", tls)
.field("retries_configuration", retries_configuration)
.field("connection_timeout", connection_timeout)
.field("response_timeout", response_timeout)
.field("protocol", protocol)
.field("database_id", database_id)
.field("tcp_settings", tcp_settings)
.field("node_address_map", node_address_map)
.field("read_routing_factory", &set_or_unset(read_routing_factory));
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
str.field("certs", &redacted_if_set(certs));
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
str.field(
"danger_accept_invalid_hostnames",
danger_accept_invalid_hostnames,
);
#[cfg(feature = "cache-aio")]
str.field("cache_config", cache_config);
#[cfg(feature = "cluster-async")]
str.field("overall_response_timeout", overall_response_timeout)
.field("connection_concurrency_limit", connection_concurrency_limit)
.field("write_backpressure_boundary", write_backpressure_boundary)
.field("max_connection_attempts", max_connection_attempts)
.field("async_push_sender", &set_or_unset(async_push_sender))
.field("async_dns_resolver", &set_or_unset(async_dns_resolver));
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
str.field("credentials_provider", &set_or_unset(credentials_provider));
str.finish()
}
}
#[derive(Clone, Debug)]
pub(crate) struct RetryParams {
pub(crate) number_of_retries: u32,
max_wait_time: u64,
min_wait_time: u64,
exponent_base: u64,
factor: u64,
}
impl Default for RetryParams {
fn default() -> Self {
const DEFAULT_RETRIES: u32 = 16;
const DEFAULT_MAX_RETRY_WAIT_TIME: u64 = 655360;
const DEFAULT_MIN_RETRY_WAIT_TIME: u64 = 1280;
const DEFAULT_EXPONENT_BASE: u64 = 2;
const DEFAULT_FACTOR: u64 = 10;
Self {
number_of_retries: DEFAULT_RETRIES,
max_wait_time: DEFAULT_MAX_RETRY_WAIT_TIME,
min_wait_time: DEFAULT_MIN_RETRY_WAIT_TIME,
exponent_base: DEFAULT_EXPONENT_BASE,
factor: DEFAULT_FACTOR,
}
}
}
fn cluster_retry_wait_upper_ms(base_wait: u64, min_wait: u64, max_wait: u64) -> u64 {
let capped = base_wait.max(min_wait + 1).min(max_wait);
if capped <= min_wait {
min_wait.saturating_add(1)
} else {
capped
}
}
impl RetryParams {
pub(crate) fn wait_time_for_retry(&self, retry: u32) -> Duration {
let base_wait = self.exponent_base.pow(retry) * self.factor;
let clamped_wait =
cluster_retry_wait_upper_ms(base_wait, self.min_wait_time, self.max_wait_time);
let jittered_wait = rand::rng().random_range(self.min_wait_time..clamped_wait);
Duration::from_millis(jittered_wait)
}
}
#[derive(Default, Clone)]
pub(crate) struct ClusterParams {
pub(crate) password: Option<ArcStr>,
pub(crate) username: Option<ArcStr>,
pub(crate) read_routing_factory: Option<Arc<dyn ReadRoutingStrategyFactory>>,
pub(crate) tls: Option<TlsMode>,
pub(crate) retry_params: RetryParams,
pub(crate) tls_params: Option<TlsConnParams>,
pub(crate) connection_timeout: Duration,
pub(crate) response_timeout: Option<Duration>,
pub(crate) protocol: Option<ProtocolVersion>,
pub(crate) database_id: Option<i64>,
#[cfg(feature = "cluster-async")]
pub(crate) async_push_sender: Option<Arc<dyn AsyncPushSender>>,
pub(crate) tcp_settings: TcpSettings,
#[cfg(feature = "cluster-async")]
pub(crate) async_dns_resolver: Option<Arc<dyn AsyncDNSResolver>>,
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
pub(crate) cache_manager: Option<CacheManager>,
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
pub(crate) credentials_provider: Option<std::sync::Arc<dyn StreamingCredentialsProvider>>,
#[cfg(feature = "cluster-async")]
pub(crate) overall_response_timeout: Option<Duration>,
#[cfg(feature = "cluster-async")]
pub(crate) connection_concurrency_limit: Option<usize>,
#[cfg(feature = "cluster-async")]
pub(crate) write_backpressure_boundary: Option<usize>,
pub(crate) node_address_map: Option<HashMap<NodeAddress, NodeAddress>>,
#[cfg(feature = "cluster-async")]
pub(crate) max_connection_attempts: Option<NonZeroUsize>,
}
impl std::fmt::Debug for ClusterParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
password,
username,
read_routing_factory,
tls,
retry_params,
tls_params,
connection_timeout,
response_timeout,
protocol,
database_id,
#[cfg(feature = "cluster-async")]
async_push_sender,
tcp_settings,
#[cfg(feature = "cluster-async")]
async_dns_resolver,
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
cache_manager,
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
credentials_provider,
#[cfg(feature = "cluster-async")]
overall_response_timeout,
#[cfg(feature = "cluster-async")]
connection_concurrency_limit,
#[cfg(feature = "cluster-async")]
write_backpressure_boundary,
node_address_map,
#[cfg(feature = "cluster-async")]
max_connection_attempts,
} = self;
let mut str = f.debug_struct("ClusterParams");
str.field("password", &redacted_if_set(password))
.field("username", username)
.field("tls", tls)
.field("retry_params", retry_params)
.field("tls_params", tls_params)
.field("connection_timeout", connection_timeout)
.field("response_timeout", response_timeout)
.field("protocol", protocol)
.field("database_id", database_id)
.field("tcp_settings", tcp_settings)
.field("node_address_map", node_address_map)
.field("read_routing_factory", &set_or_unset(read_routing_factory));
#[cfg(feature = "cluster-async")]
str.field("overall_response_timeout", overall_response_timeout)
.field("connection_concurrency_limit", connection_concurrency_limit)
.field("write_backpressure_boundary", write_backpressure_boundary)
.field("max_connection_attempts", max_connection_attempts)
.field("async_push_sender", &set_or_unset(async_push_sender))
.field("async_dns_resolver", &set_or_unset(async_dns_resolver));
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
str.field("cache_manager", &set_or_unset(cache_manager));
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
str.field("credentials_provider", &set_or_unset(credentials_provider));
str.finish()
}
}
impl ClusterParams {
fn from(value: BuilderParams) -> RedisResult<Self> {
#[cfg(not(any(feature = "tls-rustls", feature = "tls-native-tls")))]
let tls_params: Option<TlsConnParams> = None;
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
let tls_params = {
let retrieved_tls_params = value.certs.as_ref().map(retrieve_tls_certificates);
retrieved_tls_params.transpose()?
};
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
let tls_params = if value.danger_accept_invalid_hostnames {
let mut tls_params = tls_params.unwrap_or(TlsConnParams {
#[cfg(feature = "tls-rustls")]
client_tls_params: None,
#[cfg(feature = "tls-rustls")]
root_cert_store: None,
#[cfg(any(
all(feature = "tls-native-tls", not(feature = "tls-rustls")),
all(feature = "tokio-native-tls-comp", not(feature = "tokio-rustls-comp")),
all(feature = "smol-native-tls-comp", not(feature = "smol-rustls-comp"))
))]
native_tls_params: None,
danger_accept_invalid_hostnames: false,
});
tls_params.danger_accept_invalid_hostnames = true;
Some(tls_params)
} else {
tls_params
};
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
let cache_manager = value
.cache_config
.as_ref()
.map(|cache_config| CacheManager::new(*cache_config));
Ok(Self {
password: value.password,
username: value.username,
read_routing_factory: value.read_routing_factory,
tls: value.tls,
retry_params: value.retries_configuration,
tls_params,
connection_timeout: value
.connection_timeout
.unwrap_or(DEFAULT_CONNECTION_TIMEOUT.unwrap()),
response_timeout: value.response_timeout,
protocol: value.protocol,
database_id: value.database_id,
#[cfg(feature = "cluster-async")]
async_push_sender: value.async_push_sender,
tcp_settings: value.tcp_settings,
#[cfg(feature = "cluster-async")]
async_dns_resolver: value.async_dns_resolver,
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
cache_manager,
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
credentials_provider: value.credentials_provider,
#[cfg(feature = "cluster-async")]
overall_response_timeout: match value.overall_response_timeout {
OverallResponseTimeout::MatchResponseTimeout => value.response_timeout,
OverallResponseTimeout::Explicit(d) => d,
},
#[cfg(feature = "cluster-async")]
connection_concurrency_limit: value.connection_concurrency_limit,
#[cfg(feature = "cluster-async")]
write_backpressure_boundary: value.write_backpressure_boundary,
node_address_map: value.node_address_map,
#[cfg(feature = "cluster-async")]
max_connection_attempts: value.max_connection_attempts,
})
}
fn with_config(mut self, config: cluster::ClusterConfig) -> Self {
if let Some(connection_timeout) = config.connection_timeout {
self.connection_timeout = connection_timeout;
}
self.response_timeout = config.response_timeout;
#[cfg(feature = "cluster-async")]
if let Some(async_push_sender) = config.async_push_sender {
self.async_push_sender = Some(async_push_sender);
}
#[cfg(feature = "cluster-async")]
if let Some(async_dns_resolver) = config.async_dns_resolver {
self.async_dns_resolver = Some(async_dns_resolver);
}
self
}
}
pub struct ClusterClientBuilder {
initial_nodes: RedisResult<Vec<ConnectionInfo>>,
builder_params: BuilderParams,
}
impl std::fmt::Debug for ClusterClientBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
initial_nodes,
builder_params,
} = self;
f.debug_struct("ClusterClientBuilder")
.field("initial_nodes", initial_nodes)
.field("builder_params", builder_params)
.finish()
}
}
impl ClusterClientBuilder {
pub fn new<T: IntoConnectionInfo>(initial_nodes: impl IntoIterator<Item = T>) -> Self {
Self {
initial_nodes: initial_nodes
.into_iter()
.map(|x| x.into_connection_info())
.collect(),
builder_params: Default::default(),
}
}
pub fn build(self) -> RedisResult<ClusterClient> {
let initial_nodes = self.initial_nodes?;
let first_node = match initial_nodes.first() {
Some(node) => node,
None => {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Initial nodes can't be empty.",
)));
}
};
let mut cluster_params = ClusterParams::from(self.builder_params)?;
let password = if cluster_params.password.is_none() {
cluster_params
.password
.clone_from(&first_node.redis.password.as_ref().map(|str| str.into()));
&cluster_params.password
} else {
&None
};
let username = if cluster_params.username.is_none() {
cluster_params
.username
.clone_from(&first_node.redis.username.as_ref().map(|str| str.into()));
&cluster_params.username
} else {
&None
};
let tls = cluster_params.tls.is_none().then(|| {
cluster_params.tls = first_node.addr.tls_mode();
cluster_params.tls
});
let protocol = if cluster_params.protocol.is_none() {
cluster_params.protocol = Some(first_node.redis.protocol);
cluster_params.protocol
} else {
None
};
let database_id = match cluster_params.database_id {
Some(id) => id,
None => first_node.redis.db,
};
cluster_params.database_id = Some(database_id);
for node in &initial_nodes {
if let ConnectionAddr::Unix(_) = node.addr {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"This library cannot use unix socket because Redis's cluster command returns only cluster's IP and port.",
)));
}
if password.is_some() && node.redis.password.as_deref() != password.as_deref() {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Cannot use different password among initial nodes.",
)));
}
if username.is_some() && node.redis.username.as_deref() != username.as_deref() {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Cannot use different username among initial nodes.",
)));
}
if protocol.is_some() && Some(node.redis.protocol) != protocol {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Cannot use different protocol among initial nodes.",
)));
}
if node.redis.db != 0 && node.redis.db != database_id {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Cannot use different database among initial nodes.",
)));
}
if tls.is_some_and(|tls| node.addr.tls_mode() != tls) {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Cannot use different TLS modes among initial nodes.",
)));
}
}
Ok(ClusterClient {
initial_nodes,
cluster_params,
})
}
pub fn password(mut self, password: impl AsRef<str>) -> Self {
self.builder_params.password = Some(password.as_ref().into());
self
}
pub fn username(mut self, username: impl AsRef<str>) -> Self {
self.builder_params.username = Some(username.as_ref().into());
self
}
pub fn retries(mut self, retries: u32) -> Self {
self.builder_params.retries_configuration.number_of_retries = retries;
self
}
pub fn max_retry_wait(mut self, max_wait: u64) -> Self {
self.builder_params.retries_configuration.max_wait_time = max_wait;
self
}
pub fn min_retry_wait(mut self, min_wait: u64) -> Self {
self.builder_params.retries_configuration.min_wait_time = min_wait;
self
}
pub fn retry_wait_formula(mut self, factor: u64, exponent_base: u64) -> Self {
self.builder_params.retries_configuration.factor = factor;
self.builder_params.retries_configuration.exponent_base = exponent_base;
self
}
#[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
pub fn tls(mut self, tls: TlsMode) -> Self {
self.builder_params.tls = Some(tls);
self
}
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
pub fn danger_accept_invalid_hostnames(mut self, insecure: bool) -> Self {
self.builder_params.danger_accept_invalid_hostnames = insecure;
self
}
#[cfg(any(feature = "tls-rustls", feature = "tls-native-tls"))]
pub fn certs(mut self, certificates: TlsCertificates) -> Self {
if self.builder_params.tls.is_none() {
self.builder_params.tls = Some(TlsMode::Secure);
}
self.builder_params.certs = Some(certificates);
self
}
#[deprecated(note = "Use `read_routing_strategy(RandomReplicaStrategy)` instead")]
pub fn read_from_replicas(mut self) -> Self {
self.builder_params.read_routing_factory = Some(Arc::new(RandomReplicaStrategy));
self
}
pub fn read_routing_strategy(
mut self,
strategy: impl ReadRoutingStrategyFactory + 'static,
) -> Self {
self.builder_params.read_routing_factory = Some(Arc::new(strategy));
self
}
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.builder_params.connection_timeout = Some(connection_timeout);
self
}
pub fn response_timeout(mut self, response_timeout: Duration) -> Self {
self.builder_params.response_timeout = Some(response_timeout);
self
}
#[cfg(feature = "cluster-async")]
pub fn overall_response_timeout(mut self, timeout: Option<Duration>) -> Self {
self.builder_params.overall_response_timeout = OverallResponseTimeout::Explicit(timeout);
self
}
pub fn use_protocol(mut self, protocol: ProtocolVersion) -> Self {
self.builder_params.protocol = Some(protocol);
self
}
pub fn database_id(mut self, database_id: i64) -> Self {
self.builder_params.database_id = Some(database_id);
self
}
#[cfg(feature = "cluster-async")]
pub fn push_sender(mut self, push_sender: impl AsyncPushSender) -> Self {
self.builder_params.async_push_sender = Some(Arc::new(push_sender));
self
}
pub fn tcp_settings(mut self, tcp_settings: TcpSettings) -> Self {
self.builder_params.tcp_settings = tcp_settings;
self
}
pub fn node_address_map(mut self, map: HashMap<NodeAddress, NodeAddress>) -> Self {
self.builder_params.node_address_map = Some(map);
self
}
#[cfg(feature = "cluster-async")]
pub fn async_dns_resolver(mut self, resolver: impl AsyncDNSResolver) -> Self {
self.builder_params.async_dns_resolver = Some(Arc::new(resolver));
self
}
#[cfg(all(feature = "cache-aio", feature = "cluster-async"))]
pub fn cache_config(mut self, cache_config: CacheConfig) -> Self {
self.builder_params.cache_config = Some(cache_config);
self
}
#[cfg(feature = "cluster-async")]
pub fn connection_concurrency_limit(mut self, limit: usize) -> Self {
self.builder_params.connection_concurrency_limit = Some(limit);
self
}
#[cfg(feature = "cluster-async")]
pub fn write_backpressure_boundary(mut self, boundary: usize) -> Self {
self.builder_params.write_backpressure_boundary = Some(boundary);
self
}
#[cfg(all(feature = "token-based-authentication", feature = "cluster-async"))]
pub fn set_credentials_provider<P>(mut self, provider: P) -> Self
where
P: StreamingCredentialsProvider + 'static,
{
self.builder_params.credentials_provider = Some(std::sync::Arc::new(provider));
self
}
#[cfg(feature = "cluster-async")]
pub fn max_connection_attempts(mut self, max_attempts: NonZeroUsize) -> Self {
self.builder_params.max_connection_attempts = Some(max_attempts);
self
}
}
#[derive(Clone)]
pub struct ClusterClient {
initial_nodes: Vec<ConnectionInfo>,
cluster_params: ClusterParams,
}
impl std::fmt::Debug for ClusterClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
initial_nodes,
cluster_params,
} = self;
f.debug_struct("ClusterClient")
.field("initial_nodes", initial_nodes)
.field("cluster_params", cluster_params)
.finish()
}
}
impl ClusterClient {
pub fn new<T: IntoConnectionInfo>(
initial_nodes: impl IntoIterator<Item = T>,
) -> RedisResult<Self> {
Self::builder(initial_nodes).build()
}
pub fn builder<T: IntoConnectionInfo>(
initial_nodes: impl IntoIterator<Item = T>,
) -> ClusterClientBuilder {
ClusterClientBuilder::new(initial_nodes)
}
pub fn get_connection(&self) -> RedisResult<cluster::ClusterConnection> {
cluster::ClusterConnection::new(self.cluster_params.clone(), self.initial_nodes.clone())
}
pub fn get_connection_with_config(
&self,
config: cluster::ClusterConfig,
) -> RedisResult<cluster::ClusterConnection> {
cluster::ClusterConnection::new(
self.cluster_params.clone().with_config(config),
self.initial_nodes.clone(),
)
}
#[cfg(feature = "cluster-async")]
pub async fn get_async_connection(&self) -> RedisResult<cluster_async::ClusterConnection> {
cluster_async::ClusterConnection::new(&self.initial_nodes, self.cluster_params.clone())
.await
}
#[cfg(feature = "cluster-async")]
pub async fn get_async_connection_with_config(
&self,
config: cluster::ClusterConfig,
) -> RedisResult<cluster_async::ClusterConnection> {
cluster_async::ClusterConnection::new(
&self.initial_nodes,
self.cluster_params.clone().with_config(config),
)
.await
}
#[cfg(feature = "cluster-async")]
pub fn get_pending_async_connection_with_config(
&self,
config: cluster::ClusterConfig,
) -> cluster_async::ClusterConnection {
cluster_async::ClusterConnection::new_pending(
&self.initial_nodes,
self.cluster_params.clone().with_config(config),
)
}
#[doc(hidden)]
pub fn get_generic_connection<C>(&self) -> RedisResult<cluster::ClusterConnection<C>>
where
C: crate::ConnectionLike + crate::cluster::Connect + Send,
{
cluster::ClusterConnection::new(self.cluster_params.clone(), self.initial_nodes.clone())
}
#[doc(hidden)]
#[cfg(feature = "cluster-async")]
pub async fn get_async_generic_connection<C>(
&self,
) -> RedisResult<cluster_async::ClusterConnection<C>>
where
C: crate::aio::ConnectionLike
+ cluster_async::Connect
+ Clone
+ Send
+ Sync
+ Unpin
+ 'static,
{
cluster_async::ClusterConnection::new(&self.initial_nodes, self.cluster_params.clone())
.await
}
}
#[cfg(test)]
mod tests {
use super::{ClusterClient, ClusterClientBuilder, ConnectionInfo, IntoConnectionInfo};
use crate::{ErrorKind, RedisError};
#[cfg(feature = "cluster-async")]
use std::time::Duration;
fn assert_invalid_client_config_error(error: RedisError, expected_message: &str) {
assert_eq!(error.kind(), ErrorKind::InvalidClientConfig);
assert!(
error.to_string().contains(expected_message),
"unexpected error: {error}"
);
}
fn get_connection_data() -> Vec<ConnectionInfo> {
vec![
"redis://127.0.0.1:6379".into_connection_info().unwrap(),
"redis://127.0.0.1:6378".into_connection_info().unwrap(),
"redis://127.0.0.1:6377".into_connection_info().unwrap(),
]
}
fn get_connection_data_with_password() -> Vec<ConnectionInfo> {
vec![
"redis://:password@127.0.0.1:6379"
.into_connection_info()
.unwrap(),
"redis://:password@127.0.0.1:6378"
.into_connection_info()
.unwrap(),
"redis://:password@127.0.0.1:6377"
.into_connection_info()
.unwrap(),
]
}
fn get_connection_data_with_username_and_password() -> Vec<ConnectionInfo> {
vec![
"redis://user1:password@127.0.0.1:6379"
.into_connection_info()
.unwrap(),
"redis://user1:password@127.0.0.1:6378"
.into_connection_info()
.unwrap(),
"redis://user1:password@127.0.0.1:6377"
.into_connection_info()
.unwrap(),
]
}
#[test]
fn give_no_password() {
let client = ClusterClient::new(get_connection_data()).unwrap();
assert_eq!(client.cluster_params.password, None);
}
#[test]
fn give_password_by_initial_nodes() {
let client = ClusterClient::new(get_connection_data_with_password()).unwrap();
assert_eq!(client.cluster_params.password, Some("password".into()));
}
#[test]
fn give_username_and_password_by_initial_nodes() {
let client = ClusterClient::new(get_connection_data_with_username_and_password()).unwrap();
assert_eq!(client.cluster_params.password, Some("password".into()));
assert_eq!(client.cluster_params.username, Some("user1".into()));
}
#[test]
fn fail_if_received_different_password_between_initial_nodes() {
let result = ClusterClient::new(vec![
"redis://:password1@127.0.0.1:6379",
"redis://:password2@127.0.0.1:6378",
"redis://:password3@127.0.0.1:6377",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different password among initial nodes.",
);
}
#[test]
fn fail_if_received_different_username_between_initial_nodes() {
let result = ClusterClient::new(vec![
"redis://user1:password@127.0.0.1:6379",
"redis://user2:password@127.0.0.1:6378",
"redis://user1:password@127.0.0.1:6377",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different username among initial nodes.",
);
}
#[test]
fn fail_if_received_different_protocol_between_initial_nodes() {
let result = ClusterClient::new(vec![
"redis://127.0.0.1:6379/?protocol=3",
"redis://127.0.0.1:6378",
"redis://127.0.0.1:6377",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different protocol among initial nodes.",
);
}
#[test]
fn fail_if_received_different_tls_between_initial_nodes() {
let result = ClusterClient::new(vec![
"rediss://127.0.0.1:6379/",
"redis://127.0.0.1:6378",
"redis://127.0.0.1:6377",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different TLS modes among initial nodes.",
);
}
#[test]
fn fail_if_received_different_tls_after_plain_initial_node() {
let result = ClusterClient::new(vec![
"redis://127.0.0.1:6379",
"rediss://127.0.0.1:6378/",
"redis://127.0.0.1:6377",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different TLS modes among initial nodes.",
);
}
#[test]
fn give_username_password_by_method() {
let client = ClusterClientBuilder::new(get_connection_data_with_username_and_password())
.password("password")
.username("user1")
.build()
.unwrap();
assert_eq!(client.cluster_params.password, Some("password".into()));
assert_eq!(client.cluster_params.username, Some("user1".into()));
}
#[test]
fn give_empty_initial_nodes() {
let client = ClusterClient::new(Vec::<String>::new());
assert_invalid_client_config_error(client.unwrap_err(), "Initial nodes can't be empty.");
}
#[test]
fn database_id_defaults_to_zero() {
let client = ClusterClient::new(get_connection_data()).unwrap();
assert_eq!(client.cluster_params.database_id, Some(0));
}
#[test]
fn give_database_id_by_method() {
let client = ClusterClientBuilder::new(get_connection_data())
.database_id(4)
.build()
.unwrap();
assert_eq!(client.cluster_params.database_id, Some(4));
}
#[test]
fn give_database_id_by_initial_nodes() {
let client = ClusterClient::new(vec![
"redis://127.0.0.1:6379/4",
"redis://127.0.0.1:6378/4",
"redis://127.0.0.1:6377/4",
])
.unwrap();
assert_eq!(client.cluster_params.database_id, Some(4));
}
#[test]
fn fail_if_database_id_method_conflicts_with_initial_nodes() {
let result = ClusterClientBuilder::new(vec![
"redis://127.0.0.1:6379/2",
"redis://127.0.0.1:6378/2",
"redis://127.0.0.1:6377/2",
])
.database_id(7)
.build();
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different database among initial nodes.",
);
}
#[test]
fn fail_if_explicit_database_id_zero_conflicts_with_initial_nodes() {
let result = ClusterClientBuilder::new(vec![
"redis://127.0.0.1:6379/4",
"redis://127.0.0.1:6378/4",
"redis://127.0.0.1:6377/4",
])
.database_id(0)
.build();
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different database among initial nodes.",
);
}
#[test]
fn give_database_id_by_method_matching_initial_nodes() {
let client = ClusterClientBuilder::new(vec![
"redis://127.0.0.1:6379/4",
"redis://127.0.0.1:6378/4",
"redis://127.0.0.1:6377/4",
])
.database_id(4)
.build()
.unwrap();
assert_eq!(client.cluster_params.database_id, Some(4));
}
#[test]
fn give_database_id_zero_by_method_matching_initial_nodes() {
let client = ClusterClientBuilder::new(vec![
"redis://127.0.0.1:6379/0",
"redis://127.0.0.1:6378/0",
"redis://127.0.0.1:6377/0",
])
.database_id(0)
.build()
.unwrap();
assert_eq!(client.cluster_params.database_id, Some(0));
}
#[test]
fn fail_if_received_different_database_between_initial_nodes() {
let result = ClusterClient::new(vec![
"redis://127.0.0.1:6379/4",
"redis://127.0.0.1:6378/5",
"redis://127.0.0.1:6377/4",
]);
assert_invalid_client_config_error(
result.unwrap_err(),
"Cannot use different database among initial nodes.",
);
}
#[cfg(feature = "cluster-async")]
#[test]
fn overall_response_timeout_defaults_to_response_timeout() {
let client = ClusterClientBuilder::new(get_connection_data())
.response_timeout(Duration::from_secs(5))
.build()
.unwrap();
assert_eq!(
client.cluster_params.overall_response_timeout,
Some(Duration::from_secs(5))
);
}
#[cfg(feature = "cluster-async")]
#[test]
fn overall_response_timeout_can_be_disabled() {
let client = ClusterClientBuilder::new(get_connection_data())
.response_timeout(Duration::from_secs(5))
.overall_response_timeout(None)
.build()
.unwrap();
assert_eq!(client.cluster_params.overall_response_timeout, None);
}
#[cfg(feature = "cluster-async")]
#[test]
fn overall_response_timeout_can_be_set_independently() {
let client = ClusterClientBuilder::new(get_connection_data())
.response_timeout(Duration::from_secs(5))
.overall_response_timeout(Some(Duration::from_secs(30)))
.build()
.unwrap();
assert_eq!(
client.cluster_params.overall_response_timeout,
Some(Duration::from_secs(30))
);
}
#[cfg(feature = "cluster-async")]
#[test]
fn connection_concurrency_limit_default() {
let client = ClusterClient::new(get_connection_data()).unwrap();
assert_eq!(client.cluster_params.connection_concurrency_limit, None);
}
#[cfg(feature = "cluster-async")]
#[test]
fn connection_concurrency_limit_custom() {
let client = ClusterClientBuilder::new(get_connection_data())
.connection_concurrency_limit(128)
.build()
.unwrap();
assert_eq!(
client.cluster_params.connection_concurrency_limit,
Some(128)
);
}
#[cfg(feature = "cluster-async")]
#[test]
fn write_backpressure_boundary_default() {
let client = ClusterClient::new(get_connection_data()).unwrap();
assert_eq!(client.cluster_params.write_backpressure_boundary, None);
}
#[cfg(feature = "cluster-async")]
#[test]
fn write_backpressure_boundary_custom() {
let client = ClusterClientBuilder::new(get_connection_data())
.write_backpressure_boundary(16 * 1024 * 1024)
.build()
.unwrap();
assert_eq!(
client.cluster_params.write_backpressure_boundary,
Some(16 * 1024 * 1024)
);
}
#[test]
fn cluster_retry_clamp_order_caps_at_max_after_floor() {
let base = 10u64;
let min_w = 100u64;
let max_w = 50u64;
assert_eq!(base.max(min_w + 1).min(max_w), 50);
assert_eq!(base.min(max_w).max(min_w + 1), 101);
assert_eq!(
super::cluster_retry_wait_upper_ms(base, min_w, max_w),
min_w + 1
);
}
#[test]
fn cluster_retry_wait_upper_strictly_above_min_when_min_equals_max() {
assert_eq!(super::cluster_retry_wait_upper_ms(10, 100, 100), 101);
}
#[test]
fn cluster_retry_wait_time_does_not_panic_when_min_equals_max() {
let p = super::RetryParams {
number_of_retries: 16,
max_wait_time: 100,
min_wait_time: 100,
exponent_base: 2,
factor: 10,
};
let _ = p.wait_time_for_retry(0);
}
}