Skip to main content

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}