use crate::{
ClientError, Error, Result,
client::{Credentials, CredentialsProvider, CustomTransport},
};
#[cfg(feature = "native-tls")]
use native_tls::{Certificate, Identity, Protocol, TlsConnector, TlsConnectorBuilder};
use std::{
collections::HashMap,
fmt::{self, Display, Write},
path::PathBuf,
str::FromStr,
sync::Arc,
time::Duration,
};
use url::Url;
const DEFAULT_PORT: u16 = 6379;
const DEFAULT_DATABASE: usize = 0;
const DEFAULT_WAIT_BETWEEN_FAILURES: u64 = 250;
const DEFAULT_CONNECT_TIMEOUT: u64 = 10_000;
const DEFAULT_COMMAND_TIMEOUT: u64 = 0;
const DEFAULT_AUTO_RESUBSCRTBE: bool = true;
const DEFAULT_AUTO_REMONITOR: bool = true;
const DEFAULT_KEEP_ALIVE: Option<Duration> = Some(Duration::from_secs(30));
const DEFAULT_NO_DELAY: bool = true;
const DEFAULT_RETRY_ON_ERROR: bool = false;
const DEFAULT_MAX_COMMAND_ATTEMPTS: usize = 5;
const DEFAULT_MAX_MESSAGES_PER_WAVE: usize = 48;
const DEFAULT_MAX_DISCOVERY_ROUNDS: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BufferConfig {
pub read_capacity: usize,
pub tape_capacity: usize,
pub shrink_factor: usize,
pub shrink_hysteresis: usize,
}
impl BufferConfig {
pub const DEFAULT: Self = Self {
read_capacity: 64 * 1024,
tape_capacity: 64 * 1024,
shrink_factor: 8,
shrink_hysteresis: 16,
};
fn validate(&self) -> Result<()> {
if self.read_capacity == 0 {
return Err(invalid_config(
"buffers.read_capacity must be greater than 0",
));
}
if self.tape_capacity == 0 {
return Err(invalid_config(
"buffers.tape_capacity must be greater than 0",
));
}
if self.shrink_factor == 0 {
return Err(invalid_config(
"buffers.shrink_factor must be greater than 0",
));
}
if self.shrink_hysteresis == 0 {
return Err(invalid_config(
"buffers.shrink_hysteresis must be greater than 0",
));
}
Ok(())
}
}
impl Default for BufferConfig {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BackpressureConfig {
pub max_queued_bytes: usize,
pub max_pubsub_bytes: usize,
pub max_push_bytes: usize,
}
impl BackpressureConfig {
pub const DEFAULT: Self = Self {
max_queued_bytes: 16 * 1024 * 1024,
max_pubsub_bytes: 8 * 1024 * 1024,
max_push_bytes: 8 * 1024 * 1024,
};
fn validate(&self) -> Result<()> {
Ok(())
}
}
impl Default for BackpressureConfig {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RespLimits {
pub max_nesting_depth: usize,
pub max_bulk_length: usize,
pub max_collection_length: usize,
}
impl RespLimits {
pub const DEFAULT: Self = Self {
max_nesting_depth: 128,
max_bulk_length: 512 * 1024 * 1024,
max_collection_length: 128 * 1024 * 1024,
};
fn validate(&self) -> Result<()> {
if self.max_nesting_depth == 0 {
return Err(invalid_config(
"limits.max_nesting_depth must be greater than 0",
));
}
if self.max_bulk_length == 0 {
return Err(invalid_config(
"limits.max_bulk_length must be greater than 0",
));
}
if self.max_collection_length == 0 {
return Err(invalid_config(
"limits.max_collection_length must be greater than 0",
));
}
Ok(())
}
}
impl Default for RespLimits {
fn default() -> Self {
Self::DEFAULT
}
}
#[inline]
fn invalid_config(message: &'static str) -> Error {
Error::from(ClientError::InvalidConfig(message))
}
type Uri<'a> = (
&'a str,
Option<&'a str>,
Option<&'a str>,
Vec<(&'a str, u16)>,
Vec<&'a str>,
Option<HashMap<String, String>>,
);
#[derive(Clone)]
#[non_exhaustive]
pub struct Config {
pub server: ServerConfig,
pub username: Option<String>,
pub password: Option<String>,
pub credentials_provider: Option<Arc<dyn CredentialsProvider>>,
pub database: usize,
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls"))))]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub tls_config: Option<TlsConfig>,
pub connect_timeout: Duration,
pub command_timeout: Duration,
pub auto_resubscribe: bool,
pub auto_remonitor: bool,
pub connection_name: String,
pub keep_alive: Option<Duration>,
pub no_delay: bool,
pub retry_on_error: bool,
pub reconnection: ReconnectionConfig,
pub max_command_attempts: usize,
pub buffers: BufferConfig,
pub backpressure: BackpressureConfig,
pub limits: RespLimits,
pub max_messages_per_wave: usize,
#[cfg(test)]
pub(crate) send_batch_test_hook: Option<crate::network::SendBatchTestHook>,
#[cfg(test)]
pub(crate) cluster_test_hook: Option<crate::network::ClusterTestHook>,
#[cfg(test)]
pub(crate) queue_metrics_test_hook: Option<crate::network::QueueMetricsTestHook>,
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Config");
s.field("server", &self.server)
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "***"))
.field(
"credentials_provider",
&self.credentials_provider.as_ref().map(|_| "***"),
)
.field("database", &self.database);
#[cfg(any(feature = "native-tls", feature = "rustls"))]
s.field("tls_config", &self.tls_config);
s.field("connect_timeout", &self.connect_timeout)
.field("command_timeout", &self.command_timeout)
.field("auto_resubscribe", &self.auto_resubscribe)
.field("auto_remonitor", &self.auto_remonitor)
.field("connection_name", &self.connection_name)
.field("keep_alive", &self.keep_alive)
.field("no_delay", &self.no_delay)
.field("retry_on_error", &self.retry_on_error)
.field("reconnection", &self.reconnection)
.field("max_command_attempts", &self.max_command_attempts)
.field("buffers", &self.buffers)
.field("backpressure", &self.backpressure)
.field("limits", &self.limits)
.field("max_messages_per_wave", &self.max_messages_per_wave)
.finish()
}
}
impl Default for Config {
fn default() -> Self {
Self {
server: Default::default(),
username: Default::default(),
password: Default::default(),
credentials_provider: None,
database: Default::default(),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config: Default::default(),
connect_timeout: Duration::from_millis(DEFAULT_CONNECT_TIMEOUT),
command_timeout: Duration::from_millis(DEFAULT_COMMAND_TIMEOUT),
auto_resubscribe: DEFAULT_AUTO_RESUBSCRTBE,
auto_remonitor: DEFAULT_AUTO_REMONITOR,
connection_name: String::from(""),
keep_alive: DEFAULT_KEEP_ALIVE,
no_delay: DEFAULT_NO_DELAY,
retry_on_error: DEFAULT_RETRY_ON_ERROR,
reconnection: Default::default(),
max_command_attempts: DEFAULT_MAX_COMMAND_ATTEMPTS,
buffers: Default::default(),
backpressure: Default::default(),
limits: Default::default(),
max_messages_per_wave: DEFAULT_MAX_MESSAGES_PER_WAVE,
#[cfg(test)]
send_batch_test_hook: None,
#[cfg(test)]
cluster_test_hook: None,
#[cfg(test)]
queue_metrics_test_hook: None,
}
}
}
impl FromStr for Config {
type Err = Error;
fn from_str(str: &str) -> Result<Config> {
if str.contains("://") {
Self::parse_uri(str)
} else if let Some(addr) = Self::parse_addr(str) {
addr.into_config()
} else {
Err(Error::from(ClientError::ConfigParseError))
}
}
}
impl Config {
pub(crate) async fn resolve_credentials(&self) -> Result<Option<Credentials>> {
if let Some(provider) = &self.credentials_provider {
return Ok(Some(provider.credentials().await?));
}
Ok(self.password.as_ref().map(|password| Credentials {
username: self.username.clone(),
password: password.clone(),
}))
}
pub fn from_uri(uri: Url) -> Result<Config> {
Self::from_str(uri.as_str())
}
pub fn validate(&self) -> Result<()> {
self.buffers.validate()?;
self.backpressure.validate()?;
self.limits.validate()?;
if self.max_messages_per_wave == 0 {
return Err(invalid_config(
"max_messages_per_wave must be greater than 0",
));
}
if let ServerConfig::Sentinel(sentinel_config) = &self.server {
if sentinel_config.max_discovery_rounds == 0 {
return Err(invalid_config(
"sentinel max_discovery_rounds must be greater than 0",
));
}
}
Ok(())
}
fn parse_addr(str: &str) -> Option<(&str, u16)> {
if let Some(rest) = str.strip_prefix('[') {
let (host, after) = rest.split_once(']')?;
return match after {
"" => Some((host, DEFAULT_PORT)),
_ => {
let port = after.strip_prefix(':')?;
Some((host, port.parse::<u16>().ok()?))
}
};
}
let mut iter = str.split(':');
match (iter.next(), iter.next(), iter.next()) {
(Some(host), Some(port), None) => {
if let Ok(port) = port.parse::<u16>() {
Some((host, port))
} else {
None
}
}
(Some(host), None, None) => Some((host, DEFAULT_PORT)),
_ => None,
}
}
fn invalid_uri(message: String) -> Error {
Error::from(ClientError::InvalidUri(message))
}
fn take_query_param<T: FromStr>(
query: &mut HashMap<String, String>,
name: &str,
) -> Result<Option<T>> {
match query.remove(name) {
Some(value) => value.parse::<T>().map(Some).map_err(|_| {
Self::invalid_uri(format!(
"cannot parse query parameter `{name}` from `{value}`"
))
}),
None => Ok(None),
}
}
fn parse_uri(uri: &str) -> Result<Config> {
let config_parse_error = || Error::from(ClientError::ConfigParseError);
if let Some(path) = uri
.strip_prefix("unix://")
.or_else(|| uri.strip_prefix("redis+unix://"))
.or_else(|| uri.strip_prefix("redis-unix://"))
{
return Self::parse_unix_socket_uri(path);
}
let (scheme, username, password, hosts, path_segments, mut query) =
Self::break_down_uri(uri).ok_or_else(config_parse_error)?;
let mut hosts = hosts;
let mut path_segments = path_segments.into_iter();
enum ServerType {
Standalone,
Sentinel,
Cluster,
}
#[cfg(any(feature = "native-tls", feature = "rustls"))]
let (tls_config, server_type) = match scheme {
"redis" => (None, ServerType::Standalone),
"rediss" => (Some(TlsConfig::default()), ServerType::Standalone),
"redis+sentinel" | "redis-sentinel" => (None, ServerType::Sentinel),
"rediss+sentinel" | "rediss-sentinel" => {
(Some(TlsConfig::default()), ServerType::Sentinel)
}
"redis+cluster" | "redis-cluster" => (None, ServerType::Cluster),
"rediss+cluster" | "rediss-cluster" => {
(Some(TlsConfig::default()), ServerType::Cluster)
}
_ => {
return Err(config_parse_error());
}
};
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
let server_type = match scheme {
"redis" => ServerType::Standalone,
"redis+sentinel" | "redis-sentinel" => ServerType::Sentinel,
"redis+cluster" | "redis-cluster" => ServerType::Cluster,
_ => {
return Err(config_parse_error());
}
};
let server = match server_type {
ServerType::Standalone => {
if hosts.len() > 1 {
return Err(config_parse_error());
} else {
let (host, port) = hosts.pop().ok_or_else(config_parse_error)?;
ServerConfig::Standalone {
host: host.to_owned(),
port,
}
}
}
ServerType::Sentinel => {
let instances = hosts
.iter()
.map(|(host, port)| ((*host).to_owned(), *port))
.collect::<Vec<_>>();
let service_name = match path_segments.next() {
Some(service_name) => service_name.to_owned(),
None => {
return Err(config_parse_error());
}
};
let mut sentinel_config = SentinelConfig {
instances,
service_name,
..Default::default()
};
if let Some(ref mut query) = query {
if let Some(millis) =
Self::take_query_param::<u64>(query, "wait_between_failures")?
{
sentinel_config.wait_between_failures = Duration::from_millis(millis);
}
sentinel_config.username = query.remove("sentinel_username");
sentinel_config.password = query.remove("sentinel_password");
}
ServerConfig::Sentinel(sentinel_config)
}
ServerType::Cluster => {
let nodes = hosts
.iter()
.map(|(host, port)| ((*host).to_owned(), *port))
.collect::<Vec<_>>();
let mut cluster_config = ClusterConfig {
nodes,
..Default::default()
};
if let Some(ref mut query) = query
&& let Some(read_preference) =
Self::take_query_param::<ReadPreference>(query, "read_preference")?
{
cluster_config.read_preference = read_preference;
}
ServerConfig::Cluster(cluster_config)
}
};
let database = match path_segments.next() {
Some(database) => match database.parse::<usize>() {
Ok(database) => database,
Err(_) => {
return Err(config_parse_error());
}
},
None => DEFAULT_DATABASE,
};
let mut config = Config {
server,
username: username.map(percent_decode),
password: password.map(percent_decode),
database,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
tls_config,
..Default::default()
};
if let Some(ref mut query) = query {
Self::apply_query_params(&mut config, query)?;
}
Ok(config)
}
fn apply_query_params(config: &mut Config, query: &mut HashMap<String, String>) -> Result<()> {
if let Some(millis) = Self::take_query_param::<u64>(query, "connect_timeout")? {
config.connect_timeout = Duration::from_millis(millis);
}
if let Some(millis) = Self::take_query_param::<u64>(query, "command_timeout")? {
config.command_timeout = Duration::from_millis(millis);
}
if let Some(auto_resubscribe) = Self::take_query_param(query, "auto_resubscribe")? {
config.auto_resubscribe = auto_resubscribe;
}
if let Some(auto_remonitor) = Self::take_query_param(query, "auto_remonitor")? {
config.auto_remonitor = auto_remonitor;
}
if let Some(connection_name) = query.remove("connection_name") {
config.connection_name = connection_name;
}
if let Some(keep_alive) = Self::take_query_param::<u64>(query, "keep_alive")? {
config.keep_alive = (keep_alive > 0).then(|| Duration::from_millis(keep_alive));
}
if let Some(no_delay) = Self::take_query_param(query, "no_delay")? {
config.no_delay = no_delay;
}
if let Some(retry_on_error) = Self::take_query_param(query, "retry_on_error")? {
config.retry_on_error = retry_on_error;
}
if let Some(max_command_attempts) = Self::take_query_param(query, "max_command_attempts")? {
config.max_command_attempts = max_command_attempts;
}
if let Some(name) = query.keys().min() {
return Err(Self::invalid_uri(format!(
"unknown query parameter `{name}`"
)));
}
Ok(())
}
fn parse_unix_socket_uri(after_scheme: &str) -> Result<Config> {
let (path, query) = match after_scheme.split_once('?') {
Some((path, query)) => (path, Some(query)),
None => (after_scheme, None),
};
if path.is_empty() || path == "/" {
return Err(Self::invalid_uri(
"a unix socket URI needs the path of the socket".to_owned(),
));
}
let mut query = match query {
Some(query) => query
.split('&')
.map(|s| s.split_once('=').map(|(k, v)| (k.to_owned(), v.to_owned())))
.collect::<Option<HashMap<String, String>>>()
.ok_or_else(|| Error::from(ClientError::ConfigParseError))?,
None => HashMap::new(),
};
let mut config = Config {
server: ServerConfig::UnixSocket {
path: PathBuf::from(percent_decode(path)),
},
database: Self::take_query_param(&mut query, "db")?.unwrap_or(DEFAULT_DATABASE),
..Default::default()
};
Self::apply_query_params(&mut config, &mut query)?;
Ok(config)
}
#[expect(
clippy::arithmetic_side_effects,
reason = "`find` answered `Some`, so the scheme and its three-byte separator \
are both inside the string."
)]
fn break_down_uri<'a>(uri: &'a str) -> Option<Uri<'a>> {
let end_of_scheme = match uri.find("://") {
Some(index) => index,
None => {
return None;
}
};
let scheme = &uri[..end_of_scheme];
let after_scheme = &uri[end_of_scheme + 3..];
let (before_query, query) = match after_scheme.find('?') {
Some(index) => match Self::exclusive_split_at(after_scheme, index) {
(Some(before_query), after_query) => (before_query, after_query),
_ => {
return None;
}
},
None => (after_scheme, None),
};
let (authority, path) = match before_query.find('/') {
Some(index) => match Self::exclusive_split_at(before_query, index) {
(Some(authority), path) => (authority, path),
_ => {
return None;
}
},
None => (before_query, None),
};
let (user_info, hosts) = match authority.rfind('@') {
Some(index) => {
let (user_info, hosts) = Self::exclusive_split_at(authority, index);
match hosts {
Some(hosts) => (user_info, hosts),
None => {
return None;
}
}
}
None => (None, authority),
};
let (username, password) = match user_info {
Some(user_info) => match user_info.find(':') {
Some(index) => match Self::exclusive_split_at(user_info, index) {
(username, None) => (username, Some("")),
(username, password) => (username, password),
},
None => {
return None;
}
},
None => (None, None),
};
let hosts = hosts
.split(',')
.map(Self::parse_addr)
.collect::<Option<Vec<_>>>();
let hosts = hosts?;
let path_segments = match path {
Some(path) => path.split('/').collect::<Vec<_>>(),
None => Vec::new(),
};
let query = match query.map(|q| {
q.split('&')
.map(|s| s.split_once('=').map(|(k, v)| (k.to_owned(), v.to_owned())))
.collect::<Option<HashMap<String, String>>>()
}) {
Some(Some(query)) => Some(query),
Some(None) => return None,
None => None,
};
Some((scheme, username, password, hosts, path_segments, query))
}
fn exclusive_split_at(s: &str, i: usize) -> (Option<&str>, Option<&str>) {
let (l, r) = s.split_at(i);
let lout = if !l.is_empty() { Some(l) } else { None };
let rout = if r.len() > 1 { Some(&r[1..]) } else { None };
(lout, rout)
}
}
impl Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
if self.tls_config.is_some() {
match &self.server {
ServerConfig::Standalone { host: _, port: _ } => f.write_str("rediss://")?,
ServerConfig::Sentinel(_) => f.write_str("rediss+sentinel://")?,
ServerConfig::Cluster(_) => f.write_str("rediss+cluster://")?,
ServerConfig::UnixSocket { path: _ } => f.write_str("unix://")?,
ServerConfig::Custom(_) => f.write_str("custom://")?,
}
} else {
match &self.server {
ServerConfig::Standalone { host: _, port: _ } => f.write_str("redis://")?,
ServerConfig::Sentinel(_) => f.write_str("redis+sentinel://")?,
ServerConfig::Cluster(_) => f.write_str("redis+cluster://")?,
ServerConfig::UnixSocket { path: _ } => f.write_str("unix://")?,
ServerConfig::Custom(_) => f.write_str("custom://")?,
}
}
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
match &self.server {
ServerConfig::Standalone { host: _, port: _ } => f.write_str("redis://")?,
ServerConfig::Sentinel(_) => f.write_str("redis+sentinel://")?,
ServerConfig::Cluster(_) => f.write_str("redis+cluster://")?,
ServerConfig::UnixSocket { path: _ } => f.write_str("unix://")?,
ServerConfig::Custom(_) => f.write_str("custom://")?,
}
if !matches!(
&self.server,
ServerConfig::UnixSocket { .. } | ServerConfig::Custom(_)
) {
if let Some(username) = &self.username {
f.write_str(username)?;
}
if self.password.is_some() {
f.write_str(":***@")?;
}
}
match &self.server {
ServerConfig::Standalone { host, port } => {
f.write_str(host)?;
if *port != DEFAULT_PORT {
f.write_char(':')?;
f.write_str(&port.to_string())?;
}
}
ServerConfig::Sentinel(SentinelConfig {
instances,
service_name,
wait_between_failures: _,
max_discovery_rounds: _,
password: _,
username: _,
credentials_provider: _,
}) => {
f.write_str(
&instances
.iter()
.map(|(host, port)| format!("{host}:{port}"))
.collect::<Vec<String>>()
.join(","),
)?;
f.write_char('/')?;
f.write_str(service_name)?;
}
ServerConfig::Cluster(ClusterConfig {
nodes,
read_preference: _,
}) => {
f.write_str(
&nodes
.iter()
.map(|(host, port)| format!("{host}:{port}"))
.collect::<Vec<String>>()
.join(","),
)?;
}
ServerConfig::UnixSocket { path } => {
f.write_str(&path.display().to_string())?;
}
ServerConfig::Custom(_) => {}
}
let mut query_separator = false;
let database_in_path = matches!(
&self.server,
ServerConfig::Standalone { .. } | ServerConfig::Sentinel(_) | ServerConfig::Cluster(_)
);
if self.database > 0 {
if database_in_path {
f.write_char('/')?;
f.write_str(&self.database.to_string())?;
} else {
query_separator = true;
f.write_fmt(format_args!("?db={}", self.database))?;
}
}
let connect_timeout = self.connect_timeout.as_millis() as u64;
if connect_timeout != DEFAULT_CONNECT_TIMEOUT {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("connect_timeout={connect_timeout}"))?;
}
let command_timeout = self.command_timeout.as_millis() as u64;
if command_timeout != DEFAULT_COMMAND_TIMEOUT {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("command_timeout={command_timeout}"))?;
}
if self.auto_resubscribe != DEFAULT_AUTO_RESUBSCRTBE {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("auto_resubscribe={}", self.auto_resubscribe))?;
}
if self.auto_remonitor != DEFAULT_AUTO_REMONITOR {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("auto_remonitor={}", self.auto_remonitor))?;
}
if !self.connection_name.is_empty() {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("connection_name={}", self.connection_name))?;
}
if self.keep_alive != DEFAULT_KEEP_ALIVE {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
let keep_alive = self.keep_alive.unwrap_or_default().as_millis();
f.write_fmt(format_args!("keep_alive={keep_alive}"))?;
}
if self.no_delay != DEFAULT_NO_DELAY {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("no_delay={}", self.no_delay))?;
}
if self.retry_on_error != DEFAULT_RETRY_ON_ERROR {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("retry_on_error={}", self.retry_on_error))?;
}
if let ServerConfig::Cluster(ClusterConfig {
nodes: _,
read_preference,
}) = &self.server
&& *read_preference != ReadPreference::default()
{
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!("read_preference={read_preference}"))?;
}
if let ServerConfig::Sentinel(SentinelConfig {
instances: _,
service_name: _,
wait_between_failures: wait_beetween_failures,
max_discovery_rounds: _,
password,
username,
credentials_provider: _,
}) = &self.server
{
let wait_between_failures = wait_beetween_failures.as_millis() as u64;
if wait_between_failures != DEFAULT_WAIT_BETWEEN_FAILURES {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_fmt(format_args!(
"wait_between_failures={wait_between_failures}"
))?;
}
if let Some(username) = username {
if !query_separator {
query_separator = true;
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_str("sentinel_username=")?;
f.write_str(username)?;
}
if password.is_some() {
if !query_separator {
f.write_char('?')?;
} else {
f.write_char('&')?;
}
f.write_str("sentinel_password=***")?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ServerConfig {
Standalone {
host: String,
port: u16,
},
Sentinel(SentinelConfig),
Cluster(ClusterConfig),
UnixSocket {
path: PathBuf,
},
Custom(CustomTransport),
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig::Standalone {
host: "127.0.0.1".to_owned(),
port: 6379,
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct SentinelConfig {
pub instances: Vec<(String, u16)>,
pub service_name: String,
pub wait_between_failures: Duration,
pub max_discovery_rounds: usize,
pub username: Option<String>,
pub password: Option<String>,
pub credentials_provider: Option<Arc<dyn CredentialsProvider>>,
}
impl fmt::Debug for SentinelConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SentinelConfig")
.field("instances", &self.instances)
.field("service_name", &self.service_name)
.field("wait_between_failures", &self.wait_between_failures)
.field("max_discovery_rounds", &self.max_discovery_rounds)
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "***"))
.field(
"credentials_provider",
&self.credentials_provider.as_ref().map(|_| "***"),
)
.finish()
}
}
impl Default for SentinelConfig {
fn default() -> Self {
Self {
instances: Default::default(),
service_name: Default::default(),
wait_between_failures: Duration::from_millis(DEFAULT_WAIT_BETWEEN_FAILURES),
max_discovery_rounds: DEFAULT_MAX_DISCOVERY_ROUNDS,
password: None,
username: None,
credentials_provider: None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ReadPreference {
#[default]
Master,
PreferReplica,
}
impl Display for ReadPreference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadPreference::Master => f.write_str("master"),
ReadPreference::PreferReplica => f.write_str("prefer_replica"),
}
}
}
impl FromStr for ReadPreference {
type Err = Error;
fn from_str(str: &str) -> Result<Self> {
match str {
"master" => Ok(ReadPreference::Master),
"prefer_replica" => Ok(ReadPreference::PreferReplica),
_ => Err(Error::from(ClientError::InvalidUri(format!(
"unknown read preference `{str}`"
)))),
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ClusterConfig {
pub nodes: Vec<(String, u16)>,
pub read_preference: ReadPreference,
}
#[cfg(feature = "rustls")]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TlsConfig {
pub rustls_config: Arc<rustls::ClientConfig>,
}
#[cfg(feature = "rustls")]
impl Default for TlsConfig {
fn default() -> Self {
let root_store =
rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let rustls_config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Self {
rustls_config: Arc::new(rustls_config),
}
}
}
#[cfg(feature = "native-tls")]
#[derive(Clone)]
#[non_exhaustive]
pub struct TlsConfig {
identity: Option<Identity>,
root_certificates: Option<Vec<Certificate>>,
min_protocol_version: Option<Protocol>,
max_protocol_version: Option<Protocol>,
disable_built_in_roots: bool,
danger_accept_invalid_certs: bool,
danger_accept_invalid_hostnames: bool,
use_sni: bool,
}
#[cfg(feature = "native-tls")]
impl Default for TlsConfig {
fn default() -> Self {
Self {
identity: None,
root_certificates: None,
min_protocol_version: Some(Protocol::Tlsv12),
max_protocol_version: None,
disable_built_in_roots: false,
danger_accept_invalid_certs: false,
danger_accept_invalid_hostnames: false,
use_sni: true,
}
}
}
#[cfg(feature = "native-tls")]
impl std::fmt::Debug for TlsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TlsConfig")
.field("min_protocol_version", &self.min_protocol_version)
.field("max_protocol_version", &self.max_protocol_version)
.field("disable_built_in_roots", &self.disable_built_in_roots)
.field(
"danger_accept_invalid_certs",
&self.danger_accept_invalid_certs,
)
.field(
"danger_accept_invalid_hostnames",
&self.danger_accept_invalid_hostnames,
)
.field("use_sni", &self.use_sni)
.finish()
}
}
#[cfg(feature = "native-tls")]
impl TlsConfig {
pub fn identity(&mut self, identity: Identity) -> &mut Self {
self.identity = Some(identity);
self
}
pub fn root_certificates(&mut self, root_certificates: Vec<Certificate>) -> &mut Self {
self.root_certificates = Some(root_certificates);
self
}
pub fn min_protocol_version(&mut self, min_protocol_version: Protocol) -> &mut Self {
self.min_protocol_version = Some(min_protocol_version);
self
}
pub fn max_protocol_version(&mut self, max_protocol_version: Protocol) -> &mut Self {
self.max_protocol_version = Some(max_protocol_version);
self
}
pub fn disable_built_in_roots(&mut self, disable_built_in_roots: bool) -> &mut Self {
self.disable_built_in_roots = disable_built_in_roots;
self
}
pub fn danger_accept_invalid_certs(&mut self, danger_accept_invalid_certs: bool) -> &mut Self {
self.danger_accept_invalid_certs = danger_accept_invalid_certs;
self
}
pub fn use_sni(&mut self, use_sni: bool) -> &mut Self {
self.use_sni = use_sni;
self
}
pub fn danger_accept_invalid_hostnames(
&mut self,
danger_accept_invalid_hostnames: bool,
) -> &mut Self {
self.danger_accept_invalid_hostnames = danger_accept_invalid_hostnames;
self
}
pub fn into_tls_connector_builder(&self) -> TlsConnectorBuilder {
let mut builder = TlsConnector::builder();
if let Some(root_certificates) = &self.root_certificates {
for root_certificate in root_certificates {
builder.add_root_certificate(root_certificate.clone());
}
}
builder.min_protocol_version(self.min_protocol_version);
builder.max_protocol_version(self.max_protocol_version);
builder.disable_built_in_roots(self.disable_built_in_roots);
builder.danger_accept_invalid_certs(self.danger_accept_invalid_certs);
builder.danger_accept_invalid_hostnames(self.danger_accept_invalid_hostnames);
builder.use_sni(self.use_sni);
builder
}
}
pub trait IntoConfig {
fn into_config(self) -> Result<Config>;
}
impl IntoConfig for Config {
fn into_config(self) -> Result<Config> {
Ok(self)
}
}
impl<T: Into<String>> IntoConfig for (T, u16) {
fn into_config(self) -> Result<Config> {
Ok(Config {
server: ServerConfig::Standalone {
host: self.0.into(),
port: self.1,
},
..Default::default()
})
}
}
impl IntoConfig for &str {
fn into_config(self) -> Result<Config> {
Config::from_str(self)
}
}
impl IntoConfig for String {
fn into_config(self) -> Result<Config> {
Config::from_str(&self)
}
}
impl IntoConfig for Url {
fn into_config(self) -> Result<Config> {
Config::from_uri(self)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ReconnectionConfig {
Constant {
max_attempts: u32,
delay: u32,
jitter: u32,
},
Linear {
max_attempts: u32,
max_delay: u32,
delay: u32,
jitter: u32,
},
Exponential {
max_attempts: u32,
min_delay: u32,
max_delay: u32,
multiplicative_factor: u32,
jitter: u32,
},
}
const DEFAULT_JITTER_MS: u32 = 100;
const DEFAULT_DELAY_MS: u32 = 1000;
impl Default for ReconnectionConfig {
fn default() -> Self {
Self::Constant {
max_attempts: 0,
delay: DEFAULT_DELAY_MS,
jitter: DEFAULT_JITTER_MS,
}
}
}
impl ReconnectionConfig {
pub fn new_constant(max_attempts: u32, delay: u32) -> Self {
Self::Constant {
max_attempts,
delay,
jitter: DEFAULT_JITTER_MS,
}
}
pub fn new_linear(max_attempts: u32, max_delay: u32, delay: u32) -> Self {
Self::Linear {
max_attempts,
max_delay,
delay,
jitter: DEFAULT_JITTER_MS,
}
}
pub fn new_exponential(
max_attempts: u32,
min_delay: u32,
max_delay: u32,
multiplicative_factor: u32,
) -> Self {
Self::Exponential {
max_delay,
max_attempts,
min_delay,
multiplicative_factor,
jitter: DEFAULT_JITTER_MS,
}
}
pub fn set_jitter(&mut self, jitter_ms: u32) {
match self {
Self::Constant { jitter, .. } => {
*jitter = jitter_ms;
}
Self::Linear { jitter, .. } => {
*jitter = jitter_ms;
}
Self::Exponential { jitter, .. } => {
*jitter = jitter_ms;
}
}
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "`hi` and `lo` are hex digits, so `hi * 16 + lo` is at most 255, and \
`i` only advances over bytes the `get` calls above found."
)]
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%'
&& let Some(hi) = bytes.get(i + 1).and_then(|b| (*b as char).to_digit(16))
&& let Some(lo) = bytes.get(i + 2).and_then(|b| (*b as char).to_digit(16))
{
out.push((hi * 16 + lo) as u8);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod parse_tests {
#![allow(
clippy::unwrap_used,
clippy::panic,
reason = "test code: a panic is how a test reports failure"
)]
use super::*;
fn standalone(uri: &str) -> (String, u16) {
match Config::from_str(uri).unwrap().server {
ServerConfig::Standalone { host, port } => (host, port),
other => panic!("expected Standalone, got {other:?}"),
}
}
#[test]
fn ipv6_bracketed_address_with_port() {
assert_eq!(Some(("::1", 6379)), Config::parse_addr("[::1]:6379"));
assert_eq!(
Some(("2001:db8::1", 6380)),
Config::parse_addr("[2001:db8::1]:6380")
);
}
#[test]
fn ipv6_bracketed_address_without_port() {
assert_eq!(Some(("::1", DEFAULT_PORT)), Config::parse_addr("[::1]"));
}
#[test]
fn ipv4_address_still_parses() {
assert_eq!(
Some(("127.0.0.1", 6379)),
Config::parse_addr("127.0.0.1:6379")
);
assert_eq!(
Some(("localhost", DEFAULT_PORT)),
Config::parse_addr("localhost")
);
}
#[test]
fn ipv6_uri() {
assert_eq!(("::1".to_owned(), 6379), standalone("redis://[::1]:6379"));
}
fn unix_socket(uri: &str) -> Config {
let config = Config::from_str(uri).unwrap();
match &config.server {
ServerConfig::UnixSocket { .. } => config,
other => panic!("expected UnixSocket, got {other:?}"),
}
}
#[test]
fn unix_socket_uri() {
for uri in [
"unix:///var/run/redis.sock",
"redis+unix:///var/run/redis.sock",
] {
let config = unix_socket(uri);
assert!(matches!(
&config.server,
ServerConfig::UnixSocket { path } if path == &PathBuf::from("/var/run/redis.sock")
));
assert_eq!(DEFAULT_DATABASE, config.database);
}
}
#[test]
fn unix_socket_uri_round_trips_through_display() {
let config = unix_socket("unix:///var/run/redis.sock");
assert_eq!("unix:///var/run/redis.sock", config.to_string());
}
#[test]
fn unix_socket_uri_takes_its_database_from_the_query() {
let config = unix_socket("unix:///var/run/redis.sock?db=3");
assert_eq!(3, config.database);
assert_eq!("unix:///var/run/redis.sock?db=3", config.to_string());
}
#[test]
fn unix_socket_uri_accepts_the_common_query_parameters() {
let config = unix_socket("unix:///var/run/redis.sock?connection_name=app");
assert_eq!("app", config.connection_name);
}
#[test]
fn a_unix_socket_uri_without_a_path_is_rejected() {
assert!(Config::from_str("unix://").is_err());
assert!(Config::from_str("unix:///").is_err());
}
#[test]
fn a_custom_transport_displays_opaquely() {
let config = Config {
server: ServerConfig::Custom(CustomTransport::new(|| async {
Err(Error::from(ClientError::ConfigParseError))
})),
..Default::default()
};
assert_eq!("custom://", config.to_string());
}
#[test]
fn percent_decoded_password() {
let config = Config::from_str("redis://user:p%40ss@127.0.0.1:6379").unwrap();
assert_eq!(Some("user".to_owned()), config.username);
assert_eq!(Some("p@ss".to_owned()), config.password);
}
}