glide/config/cluster.rs
1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Configuration for the **cluster** client.
3
4use super::common::{
5 BackoffStrategy, ClientIdentity, NodeAddress, PeriodicChecks, ProtocolVersion,
6 PubSubSubscriptions, ReadFrom, ServerCredentials, TlsConfig, credentials_from_info,
7 duration_as_millis_u32, from_redis_protocol, impl_common_config_builders,
8 split_connection_addr, to_redis_connection_info,
9};
10use glide_core::client::ConnectionRequest;
11use std::time::Duration;
12
13/// Configuration for a **cluster** GLIDE client.
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct GlideClusterClientConfiguration {
17 /// Seed node addresses.
18 pub addresses: Vec<NodeAddress>,
19 /// TLS mode.
20 pub tls: TlsConfig,
21 /// Optional credentials.
22 pub credentials: Option<ServerCredentials>,
23 /// Read strategy.
24 pub read_from: ReadFrom,
25 /// Overall request timeout.
26 pub request_timeout: Option<Duration>,
27 /// Connection establishment timeout.
28 pub connection_timeout: Option<Duration>,
29 /// Reconnection backoff strategy.
30 pub reconnect_strategy: Option<BackoffStrategy>,
31 /// Client name reported to the server.
32 pub client_name: Option<String>,
33 /// RESP protocol version.
34 pub protocol: ProtocolVersion,
35 /// Periodic topology checks.
36 pub periodic_checks: PeriodicChecks,
37 /// Maximum number of concurrent in-flight requests.
38 pub inflight_requests_limit: Option<u32>,
39 /// Defer connecting until the first command is issued.
40 pub lazy_connect: bool,
41 /// Pub/Sub subscriptions to establish on connect.
42 pub pubsub_subscriptions: Option<PubSubSubscriptions>,
43 /// Force creation of the Pub/Sub push channel even when no subscriptions are
44 /// configured, so runtime `subscribe`/`unsubscribe` can receive messages.
45 /// See [`crate::commands::pubsub::PubSubCommands`].
46 pub force_pubsub_channel: bool,
47 /// Custom CA certificate(s) (PEM bytes) to trust when verifying the server
48 /// under [`TlsConfig::SecureTls`]. Lowered into
49 /// `ConnectionRequest::root_certs`. Empty = use the system trust store.
50 pub root_certs: Vec<Vec<u8>>,
51 /// Client certificate + private key for **mutual TLS**. Set via
52 /// [`Self::client_identity`]. Only meaningful together with
53 /// [`TlsConfig::SecureTls`].
54 pub client_identity: Option<ClientIdentity>,
55}
56
57impl_common_config_builders!(GlideClusterClientConfiguration);
58
59impl GlideClusterClientConfiguration {
60 /// Create a cluster configuration for the given addresses.
61 pub fn new(addresses: Vec<NodeAddress>) -> Self {
62 GlideClusterClientConfiguration {
63 addresses,
64 tls: TlsConfig::NoTls,
65 credentials: None,
66 read_from: ReadFrom::Primary,
67 request_timeout: None,
68 connection_timeout: None,
69 reconnect_strategy: None,
70 client_name: None,
71 protocol: ProtocolVersion::default(),
72 periodic_checks: PeriodicChecks::Enabled,
73 inflight_requests_limit: None,
74 lazy_connect: false,
75 pubsub_subscriptions: None,
76 force_pubsub_channel: false,
77 root_certs: Vec::new(),
78 client_identity: None,
79 }
80 }
81
82 /// Build a configuration from a connection URL.
83 ///
84 /// Supports Redis URLs:
85 ///
86 /// ```
87 /// use glide::GlideClusterClientConfiguration;
88 /// let url = "redis://user:pass@localhost:6379";
89 /// let cfg = GlideClusterClientConfiguration::from_url(url).unwrap();
90 /// assert_eq!(cfg.addresses.len(), 1);
91 /// ```
92 ///
93 /// `rediss://` enables TLS with full verification;
94 /// `rediss://…/#insecure` disables certificate verification.
95 ///
96 /// Does not support Unix-socket URLs (`unix://` or `unix+redis://`).
97 /// Does not support Valkey URLs (`valkey://` or `valkeys://`).
98 // TODO #7031: Accept `valkey://` and `valkeys://` schemes.
99 pub fn from_url(url: impl AsRef<str>) -> crate::ValkeyResult<Self> {
100 Self::from_urls([url])
101 }
102
103 /// Build a configuration from one or more connection URL.
104 ///
105 /// Configuration must be identical across all URLs: conflicting settings are
106 /// rejected with a configuration error. A URL selecting a non-zero database is
107 /// also rejected because clusters only support database 0.
108 ///
109 /// The RESP `protocol` is taken from the **first** URL and not cross-validated.
110 ///
111 /// Supports Redis URLs:
112 ///
113 /// ```
114 /// use glide::GlideClusterClientConfiguration;
115 /// let urls = ["redis://user:pass@localhost:6379", "redis://user:pass@localhost:6380"];
116 /// let cfg = GlideClusterClientConfiguration::from_urls(urls).unwrap();
117 /// assert_eq!(cfg.addresses.len(), 2);
118 /// ```
119 ///
120 /// `rediss://` enables TLS with full verification;
121 /// `rediss://…/#insecure` disables certificate verification.
122 ///
123 /// Does not support Unix-socket URLs (`unix://` or `unix+redis://`).
124 /// Does not support Valkey URLs (`valkey://` or `valkeys://`).
125 // TODO #7031: Accept `valkey://` and `valkeys://` schemes.
126 pub fn from_urls<S: AsRef<str>>(
127 urls: impl IntoIterator<Item = S>,
128 ) -> crate::ValkeyResult<Self> {
129 let mut addresses = Vec::new();
130 let mut first: Option<(TlsConfig, redis::RedisConnectionInfo)> = None;
131
132 for url in urls {
133 let info = to_redis_connection_info(url)?;
134 let (address, tls) = split_connection_addr(info.addr)?;
135 addresses.push(address);
136
137 // Reject conflicting per-URL settings, matching the fork's
138 // `ClusterClient` validation — silently ignoring the settings of
139 // URLs 2..N would misconfigure the client.
140 match &first {
141 None => first = Some((tls, info.redis)),
142 Some((first_tls, first_redis)) => {
143 if info.redis.password != first_redis.password {
144 return Err(crate::error::GlideError::Configuration(
145 "Cannot use different password among initial nodes.".into(),
146 ));
147 }
148 if info.redis.username != first_redis.username {
149 return Err(crate::error::GlideError::Configuration(
150 "Cannot use different username among initial nodes.".into(),
151 ));
152 }
153 if info.redis.client_name != first_redis.client_name {
154 return Err(crate::error::GlideError::Configuration(
155 "Cannot use different client_name among initial nodes.".into(),
156 ));
157 }
158 if info.redis.db != first_redis.db {
159 return Err(crate::error::GlideError::Configuration(
160 "Cannot use different database among initial nodes.".into(),
161 ));
162 }
163 if tls != *first_tls {
164 return Err(crate::error::GlideError::Configuration(
165 "Cannot use different TLS modes among initial nodes.".into(),
166 ));
167 }
168 }
169 }
170 }
171
172 let Some((tls, redis_info)) = first else {
173 return Err(crate::error::GlideError::Configuration(
174 "at least one node URL is required".into(),
175 ));
176 };
177
178 if redis_info.db != 0 {
179 return Err(crate::error::GlideError::Configuration(
180 "cluster deployments only support database 0".into(),
181 ));
182 }
183
184 let mut cfg = Self::new(addresses).tls(tls);
185 cfg.protocol = from_redis_protocol(redis_info.protocol);
186 cfg.client_name = redis_info.client_name;
187 cfg.credentials = credentials_from_info(redis_info.username, redis_info.password);
188
189 Ok(cfg)
190 }
191
192 /// Set periodic checks.
193 pub fn periodic_checks(mut self, checks: PeriodicChecks) -> Self {
194 self.periodic_checks = checks;
195 self
196 }
197
198 pub(crate) fn to_request(&self) -> ConnectionRequest {
199 let mut req = self.common_request();
200 req.cluster_mode_enabled = true;
201 req.periodic_checks = Some(self.periodic_checks.to_core());
202 req
203 }
204}