Skip to main content

glide/config/
common.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Configuration types shared by the standalone and cluster configurations.
3
4use glide_core::client::AuthenticationInfo;
5use glide_core::client::ConnectionRetryStrategy;
6use glide_core::client::IamAuthenticationConfig;
7use glide_core::client::NodeAddress as CoreNodeAddress;
8use glide_core::client::NodeDiscoveryMode as CoreNodeDiscoveryMode;
9use glide_core::client::PeriodicCheck;
10use glide_core::client::ReadFrom as CoreReadFrom;
11use glide_core::client::TlsMode;
12use glide_core::iam::ServiceType as CoreServiceType;
13use std::collections::HashMap;
14use std::collections::HashSet;
15use std::time::Duration;
16
17/// Library name reported to the server.
18pub(crate) const LIB_NAME: &str = "GlideRust";
19
20/// Library version reported to the server.
21pub(crate) const LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
22
23/// The kind of a Pub/Sub channel subscription.
24///
25/// Mirrors Python's `PubSubChannelModes`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum PubSubChannelMode {
28    /// Exact channel name (`SUBSCRIBE`).
29    Exact,
30    /// Glob pattern (`PSUBSCRIBE`).
31    Pattern,
32    /// Shard channel (`SSUBSCRIBE`, cluster only).
33    Sharded,
34}
35
36impl PubSubChannelMode {
37    fn to_core(self) -> redis::PubSubSubscriptionKind {
38        match self {
39            PubSubChannelMode::Exact => redis::PubSubSubscriptionKind::Exact,
40            PubSubChannelMode::Pattern => redis::PubSubSubscriptionKind::Pattern,
41            PubSubChannelMode::Sharded => redis::PubSubSubscriptionKind::Sharded,
42        }
43    }
44}
45
46/// Pub/Sub subscriptions to establish automatically when the client connects.
47///
48/// Messages received on these subscriptions are delivered via
49/// [`crate::GlideClient::get_pubsub_message`] /
50/// [`crate::GlideClient::try_get_pubsub_message`] (and the cluster equivalents).
51///
52/// Mirrors Python's `*ClientConfiguration.pubsub_subscriptions`.
53#[derive(Debug, Clone, Default)]
54pub struct PubSubSubscriptions {
55    channels: HashMap<PubSubChannelMode, HashSet<Vec<u8>>>,
56}
57
58impl PubSubSubscriptions {
59    /// Create an empty subscription set.
60    pub fn new() -> Self {
61        PubSubSubscriptions::default()
62    }
63
64    /// Subscribe to `channel` under the given `mode`.
65    pub fn subscribe(mut self, mode: PubSubChannelMode, channel: impl Into<Vec<u8>>) -> Self {
66        self.channels
67            .entry(mode)
68            .or_default()
69            .insert(channel.into());
70        self
71    }
72
73    /// Whether any subscriptions are configured.
74    pub fn is_empty(&self) -> bool {
75        self.channels.values().all(|s| s.is_empty())
76    }
77
78    pub(crate) fn to_core(&self) -> redis::PubSubSubscriptionInfo {
79        let mut info: redis::PubSubSubscriptionInfo = HashMap::new();
80        for (mode, set) in &self.channels {
81            info.insert(mode.to_core(), set.clone());
82        }
83        info
84    }
85}
86
87/// The RESP protocol version used to communicate with the server.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum ProtocolVersion {
90    /// RESP2 — the legacy protocol.
91    RESP2,
92    /// RESP3 — the default; enables richer typed replies and client-side push.
93    #[default]
94    RESP3,
95}
96
97impl ProtocolVersion {
98    pub(crate) fn to_core(self) -> redis::ProtocolVersion {
99        match self {
100            ProtocolVersion::RESP2 => redis::ProtocolVersion::RESP2,
101            ProtocolVersion::RESP3 => redis::ProtocolVersion::RESP3,
102        }
103    }
104}
105
106/// Strategy for selecting which node to read from.
107#[derive(Debug, Clone, PartialEq, Eq, Default)]
108#[non_exhaustive]
109pub enum ReadFrom {
110    /// Always read from the primary.
111    #[default]
112    Primary,
113    /// Prefer replicas, falling back to the primary if none are available.
114    PreferReplica,
115    /// Read from a replica in the given availability zone when possible.
116    AZAffinity(String),
117    /// Read from a replica in the given AZ, else the primary in that AZ, else any.
118    AZAffinityReplicasAndPrimary(String),
119    /// Spread reads across all nodes (primaries and replicas) in the
120    /// given AZ, falling back to all nodes if none in the AZ are available.
121    AZAffinityAllNodes(String),
122    /// Spread reads across all nodes.
123    AllNodes,
124}
125
126impl ReadFrom {
127    pub(crate) fn to_core(&self) -> CoreReadFrom {
128        match self {
129            ReadFrom::Primary => CoreReadFrom::Primary,
130            ReadFrom::PreferReplica => CoreReadFrom::PreferReplica,
131            ReadFrom::AZAffinity(az) => CoreReadFrom::AZAffinity(az.clone()),
132            ReadFrom::AZAffinityReplicasAndPrimary(az) => {
133                CoreReadFrom::AZAffinityReplicasAndPrimary(az.clone())
134            }
135            ReadFrom::AZAffinityAllNodes(az) => CoreReadFrom::AZAffinityAllNodes(az.clone()),
136            ReadFrom::AllNodes => CoreReadFrom::AllNodes,
137        }
138    }
139}
140
141/// Controls how the standalone client discovers node roles and topology.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
143#[non_exhaustive]
144pub enum NodeDiscoveryMode {
145    /// Verify node roles via `INFO REPLICATION`, using only the provided addresses.
146    #[default]
147    Standard,
148
149    /// Skip role detection and trust the provided addresses as-is.
150    /// For proxies (e.g. Envoy) or known-static topologies.
151    Static,
152
153    /// Discover the full topology from any starting node.
154    DiscoverAll,
155}
156
157impl NodeDiscoveryMode {
158    pub(crate) fn to_core(self) -> CoreNodeDiscoveryMode {
159        match self {
160            NodeDiscoveryMode::Standard => CoreNodeDiscoveryMode::Standard,
161            NodeDiscoveryMode::Static => CoreNodeDiscoveryMode::Static,
162            NodeDiscoveryMode::DiscoverAll => CoreNodeDiscoveryMode::DiscoverAll,
163        }
164    }
165}
166
167/// A server address (host + port).
168///
169/// Mirrors Python `NodeAddress`. Defaults to `localhost:6379`.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct NodeAddress {
172    /// Hostname or IP address.
173    pub host: String,
174    /// TCP port.
175    pub port: u16,
176}
177
178impl NodeAddress {
179    /// Create a new address.
180    pub fn new(host: impl Into<String>, port: u16) -> Self {
181        NodeAddress {
182            host: host.into(),
183            port,
184        }
185    }
186}
187
188impl Default for NodeAddress {
189    fn default() -> Self {
190        NodeAddress::new("localhost", 6379)
191    }
192}
193
194impl NodeAddress {
195    pub(crate) fn to_core(&self) -> CoreNodeAddress {
196        CoreNodeAddress {
197            host: self.host.clone(),
198            port: self.port,
199        }
200    }
201}
202
203/// Server credentials for connection.
204/// Username/password or IAM credentials.
205#[derive(Clone, Default, PartialEq, Eq)]
206pub struct ServerCredentials {
207    // Username for authentication.
208    // If omitted, the default user is used.
209    // Required for IAM authentication.
210    username: Option<String>,
211    // Password for authentication.
212    // Mutually exclusive with `iam_config`.
213    pub(crate) password: Option<String>,
214    // AWS IAM authentication configuration.
215    // Mutually exclusive with `password`.
216    iam_config: Option<IamAuthConfig>,
217}
218
219impl ServerCredentials {
220    /// Password-based authentication with default username.
221    /// Mutually exclusive with IAM (see [`Self::iam`]).
222    pub fn password(password: impl Into<String>) -> Self {
223        ServerCredentials {
224            username: None,
225            password: Some(password.into()),
226            iam_config: None,
227        }
228    }
229
230    /// Password-based authentication with username.
231    /// Mutually exclusive with IAM (see [`Self::iam`]).
232    pub fn username_password(username: impl Into<String>, password: impl Into<String>) -> Self {
233        ServerCredentials {
234            username: Some(username.into()),
235            password: Some(password.into()),
236            iam_config: None,
237        }
238    }
239
240    /// AWS IAM authentication credentials.
241    pub fn iam(username: impl Into<String>, iam_config: IamAuthConfig) -> Self {
242        ServerCredentials {
243            username: Some(username.into()),
244            password: None,
245            iam_config: Some(iam_config),
246        }
247    }
248
249    /// Username for authentication.
250    pub fn username(&self) -> Option<&str> {
251        self.username.as_deref()
252    }
253
254    /// AWS IAM authentication configuration.
255    pub fn iam_config(&self) -> Option<&IamAuthConfig> {
256        self.iam_config.as_ref()
257    }
258
259    /// Whether these are IAM credentials.
260    pub fn is_iam_auth(&self) -> bool {
261        self.iam_config.is_some()
262    }
263
264    pub(crate) fn to_core(&self) -> AuthenticationInfo {
265        AuthenticationInfo {
266            username: self.username.clone(),
267            password: self.password.clone(),
268            iam_config: self.iam_config.as_ref().map(IamAuthConfig::to_core),
269        }
270    }
271}
272
273impl std::fmt::Debug for ServerCredentials {
274    /// Redacts the password so it never leaks through `{:?}` (including via the
275    /// containing configuration's derived `Debug`).
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        f.debug_struct("ServerCredentials")
278            .field("username", &self.username)
279            .field("password", &self.password.as_ref().map(|_| "<redacted>"))
280            .field("iam_config", &self.iam_config)
281            .finish()
282    }
283}
284
285/// Client certificate + private key (both PEM) for **mutual TLS**.
286///
287/// Fields are private so a half-set identity (cert without key or vice versa)
288/// is unrepresentable. Build with [`Self::new`] or the configs'
289/// `client_identity(cert, key)` builder methods.
290#[derive(Clone)]
291pub struct ClientIdentity {
292    cert_pem: Vec<u8>,
293    key_pem: Vec<u8>,
294}
295
296impl ClientIdentity {
297    /// Create a client identity from certificate and private-key PEM bytes.
298    pub fn new(cert_pem: impl Into<Vec<u8>>, key_pem: impl Into<Vec<u8>>) -> Self {
299        ClientIdentity {
300            cert_pem: cert_pem.into(),
301            key_pem: key_pem.into(),
302        }
303    }
304
305    /// The client certificate (PEM bytes).
306    pub fn cert_pem(&self) -> &[u8] {
307        &self.cert_pem
308    }
309
310    pub(crate) fn key_pem(&self) -> &[u8] {
311        &self.key_pem
312    }
313}
314
315impl std::fmt::Debug for ClientIdentity {
316    /// Redacts the private key so it never leaks through `{:?}` (including via
317    /// the containing configuration's derived `Debug`).
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.debug_struct("ClientIdentity")
320            .field("cert_pem", &format_args!("[{} bytes]", self.cert_pem.len()))
321            .field("key_pem", &"<redacted>")
322            .finish()
323    }
324}
325
326/// AWS service backing IAM authentication.
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328#[non_exhaustive]
329pub enum ServiceType {
330    /// Amazon ElastiCache.
331    ElastiCache,
332    /// Amazon MemoryDB.
333    MemoryDB,
334}
335
336impl ServiceType {
337    pub(crate) fn to_core(self) -> CoreServiceType {
338        match self {
339            ServiceType::ElastiCache => CoreServiceType::ElastiCache,
340            ServiceType::MemoryDB => CoreServiceType::MemoryDB,
341        }
342    }
343}
344
345/// AWS IAM authentication configuration.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct IamAuthConfig {
348    // AWS cluster name.
349    cluster_name: String,
350    // AWS region of the cluster (e.g. `us-east-1`).
351    region: String,
352    // Which AWS service backs the cluster.
353    service_type: ServiceType,
354    // Token refresh interval in seconds (1s–12h).
355    // `None` uses the core default.
356    refresh_interval_seconds: Option<u32>,
357}
358
359impl IamAuthConfig {
360    /// Create an IAM config for the given cluster, region, and service,
361    /// using the default refresh interval.
362    pub fn new(
363        cluster_name: impl Into<String>,
364        region: impl Into<String>,
365        service_type: ServiceType,
366    ) -> Self {
367        IamAuthConfig {
368            cluster_name: cluster_name.into(),
369            region: region.into(),
370            service_type,
371            refresh_interval_seconds: None,
372        }
373    }
374
375    /// Override the token refresh interval (seconds). Builder form.
376    #[must_use]
377    pub fn with_refresh_interval_seconds(mut self, seconds: u32) -> Self {
378        self.refresh_interval_seconds = Some(seconds);
379        self
380    }
381
382    /// The cluster name.
383    pub fn cluster_name(&self) -> &str {
384        &self.cluster_name
385    }
386
387    /// The AWS region.
388    pub fn region(&self) -> &str {
389        &self.region
390    }
391
392    /// The AWS service backing the cluster.
393    pub fn service_type(&self) -> ServiceType {
394        self.service_type
395    }
396
397    /// The token refresh interval in seconds, if overridden.
398    pub fn refresh_interval_seconds(&self) -> Option<u32> {
399        self.refresh_interval_seconds
400    }
401
402    fn to_core(&self) -> IamAuthenticationConfig {
403        IamAuthenticationConfig {
404            cluster_name: self.cluster_name.clone(),
405            region: self.region.clone(),
406            service_type: self.service_type.to_core(),
407            refresh_interval_seconds: self.refresh_interval_seconds,
408            // The Rust public API wrapper does not expose a custom credentials provider;
409            // the field is always None on this path.
410            credentials_provider: None,
411        }
412    }
413}
414
415/// Reconnection backoff strategy.
416///
417/// Mirrors Python `BackoffStrategy`.
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub struct BackoffStrategy {
420    /// Number of retry attempts before giving up on a reconnection burst.
421    pub num_of_retries: u32,
422    /// The exponent base used for the exponential backoff.
423    pub factor: u32,
424    /// The multiplier that will be applied to the waiting time between retries.
425    pub exponent_base: u32,
426    /// Optional jitter percentage applied to the computed delay.
427    pub jitter_percent: Option<u32>,
428}
429
430impl BackoffStrategy {
431    pub(crate) fn to_core(self) -> ConnectionRetryStrategy {
432        ConnectionRetryStrategy {
433            exponent_base: self.exponent_base,
434            factor: self.factor,
435            number_of_retries: self.num_of_retries,
436            jitter_percent: self.jitter_percent,
437        }
438    }
439}
440
441/// Periodic cluster topology check configuration (cluster only).
442///
443/// Mirrors Python `PeriodicChecksStatus` / `PeriodicChecksManualInterval`.
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
445pub enum PeriodicChecks {
446    /// Enabled with the default interval.
447    #[default]
448    Enabled,
449    /// Disabled entirely.
450    Disabled,
451    /// Enabled with a manual interval (seconds).
452    ManualInterval(u64),
453}
454
455impl PeriodicChecks {
456    pub(crate) fn to_core(self) -> PeriodicCheck {
457        match self {
458            PeriodicChecks::Enabled => PeriodicCheck::Enabled,
459            PeriodicChecks::Disabled => PeriodicCheck::Disabled,
460            PeriodicChecks::ManualInterval(secs) => {
461                PeriodicCheck::ManualInterval(Duration::from_secs(secs))
462            }
463        }
464    }
465}
466
467/// TLS mode.
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
469pub enum TlsConfig {
470    /// No TLS (plaintext).
471    #[default]
472    NoTls,
473    /// TLS with full certificate verification.
474    SecureTls,
475    /// TLS without certificate verification (testing only).
476    InsecureTls,
477}
478
479impl TlsConfig {
480    pub(crate) fn to_core(self) -> TlsMode {
481        match self {
482            TlsConfig::NoTls => TlsMode::NoTls,
483            TlsConfig::SecureTls => TlsMode::SecureTls,
484            TlsConfig::InsecureTls => TlsMode::InsecureTls,
485        }
486    }
487}
488
489// ---- shared request-lowering helpers -------------------------------------------
490
491/// Parse a connection URL string into `ConnectionInfo`.
492pub(crate) fn to_redis_connection_info(
493    url: impl AsRef<str>,
494) -> crate::ValkeyResult<redis::ConnectionInfo> {
495    redis::IntoConnectionInfo::into_connection_info(url.as_ref())
496        .map_err(|e| crate::error::GlideError::Configuration(e.to_string()))
497}
498
499/// Map a [`redis::ConnectionAddr`] to our address + TLS mode.
500pub(crate) fn split_connection_addr(
501    addr: redis::ConnectionAddr,
502) -> crate::ValkeyResult<(NodeAddress, TlsConfig)> {
503    match addr {
504        redis::ConnectionAddr::Tcp(host, port) => {
505            Ok((NodeAddress::new(host, port), TlsConfig::NoTls))
506        }
507        redis::ConnectionAddr::TcpTls {
508            host,
509            port,
510            insecure,
511            tls_params,
512        } => {
513            // The fork's `TlsConnParams` fields (root cert store, client
514            // identity) are `pub(crate)` — we cannot read them to map onto
515            // `root_certs` / `client_identity`. Silently dropping them would
516            // yield a mysteriously misconfigured connection (wrong trust
517            // roots, mTLS not attempted), so fail loudly instead.
518            if tls_params.is_some() {
519                return Err(crate::error::GlideError::Configuration(
520                    "ConnectionInfo carries TLS certificate parameters (TlsCertificates) that \
521                     cannot be mapped; configure them via the config's `root_certs` field and \
522                     `client_identity(cert, key)` instead"
523                        .into(),
524                ));
525            }
526            let tls = if insecure {
527                TlsConfig::InsecureTls
528            } else {
529                TlsConfig::SecureTls
530            };
531            Ok((NodeAddress::new(host, port), tls))
532        }
533        redis::ConnectionAddr::Unix(_) => Err(crate::error::GlideError::Configuration(
534            "unix-socket connections are not supported by glide-core".into(),
535        )),
536    }
537}
538
539/// Map the fork's protocol enum to ours.
540pub(crate) fn from_redis_protocol(p: redis::ProtocolVersion) -> ProtocolVersion {
541    match p {
542        redis::ProtocolVersion::RESP2 => ProtocolVersion::RESP2,
543        redis::ProtocolVersion::RESP3 => ProtocolVersion::RESP3,
544    }
545}
546
547/// Build [`ServerCredentials`] from URL-provided username/password (either may
548/// be absent; `redis://:pass@host` yields password-only credentials).
549pub(crate) fn credentials_from_info(
550    username: Option<String>,
551    password: Option<String>,
552) -> Option<ServerCredentials> {
553    match (username, password) {
554        (None, None) => None,
555        (username, password) => Some(ServerCredentials {
556            username,
557            password,
558            iam_config: None,
559        }),
560    }
561}
562
563/// Convert a [`Duration`] to whole milliseconds as `u32`, saturating at
564/// `u32::MAX` rather than silently truncating (a `Duration` above ~49.7 days
565/// would overflow `u32` ms). The core treats the timeout as a `u32` millisecond
566/// count, so saturation is the safe, lossless-within-range behavior.
567pub(crate) fn duration_as_millis_u32(d: Duration) -> u32 {
568    u32::try_from(d.as_millis()).unwrap_or(u32::MAX)
569}
570
571// ---- shared builder setters + request lowering ----------------------------------
572
573/// Generates the builder setters and the `ConnectionRequest` lowering shared by
574/// [`crate::config::GlideClientConfiguration`] and
575/// [`crate::config::GlideClusterClientConfiguration`].
576///
577/// Both structs carry the same common public fields (same names, same types), so
578/// the generated methods access them directly. Mode-specific fields/setters
579/// stay in each struct's own `impl` block, as does `to_request()`, which starts
580/// from the generated generated `common_request` and layers the mode-specific
581/// fields on top.
582macro_rules! impl_common_config_builders {
583    ($ty:ty) => {
584        impl $ty {
585            /// Configure for a single `host:port`.
586            pub fn with_address(host: impl Into<String>, port: u16) -> Self {
587                Self::new(vec![NodeAddress::new(host, port)])
588            }
589
590            /// Set TLS mode.
591            pub fn tls(mut self, tls: TlsConfig) -> Self {
592                self.tls = tls;
593                self
594            }
595
596            /// Trust a custom CA certificate (PEM bytes) when verifying the server with
597            /// [`TlsConfig::SecureTls`]. Enables secure TLS against a server presenting a
598            /// certificate signed by a private/self-signed CA. May be called more than
599            /// once to trust multiple CAs.
600            pub fn tls_ca_cert(mut self, pem: impl Into<Vec<u8>>) -> Self {
601                self.root_certs.push(pem.into());
602                self
603            }
604
605            /// Set a client certificate + private key (both PEM) for **mutual TLS**.
606            /// The server must be configured to require/verify client certificates.
607            /// Only meaningful together with [`TlsConfig::SecureTls`].
608            #[must_use]
609            pub fn client_identity(
610                mut self,
611                cert_pem: impl Into<Vec<u8>>,
612                key_pem: impl Into<Vec<u8>>,
613            ) -> Self {
614                self.client_identity = Some(ClientIdentity::new(cert_pem, key_pem));
615                self
616            }
617
618            /// Set credentials.
619            pub fn credentials(mut self, creds: ServerCredentials) -> Self {
620                self.credentials = Some(creds);
621                self
622            }
623
624            /// Set read strategy.
625            pub fn read_from(mut self, read_from: ReadFrom) -> Self {
626                self.read_from = read_from;
627                self
628            }
629
630            /// Set request timeout.
631            pub fn request_timeout(mut self, timeout: Duration) -> Self {
632                self.request_timeout = Some(timeout);
633                self
634            }
635
636            /// Set the connection-establishment timeout.
637            pub fn connection_timeout(mut self, timeout: Duration) -> Self {
638                self.connection_timeout = Some(timeout);
639                self
640            }
641
642            /// Set the maximum number of concurrent in-flight requests.
643            pub fn inflight_requests_limit(mut self, limit: u32) -> Self {
644                self.inflight_requests_limit = Some(limit);
645                self
646            }
647
648            /// Set client name.
649            pub fn client_name(mut self, name: impl Into<String>) -> Self {
650                self.client_name = Some(name.into());
651                self
652            }
653
654            /// Set protocol version.
655            pub fn protocol(mut self, protocol: ProtocolVersion) -> Self {
656                self.protocol = protocol;
657                self
658            }
659
660            /// Enable lazy connection.
661            pub fn lazy_connect(mut self, lazy: bool) -> Self {
662                self.lazy_connect = lazy;
663                self
664            }
665
666            /// Set reconnection strategy.
667            pub fn reconnect_strategy(mut self, strategy: BackoffStrategy) -> Self {
668                self.reconnect_strategy = Some(strategy);
669                self
670            }
671
672            /// Set Pub/Sub subscriptions to establish on connect.
673            pub fn subscriptions(mut self, subscriptions: PubSubSubscriptions) -> Self {
674                self.pubsub_subscriptions = Some(subscriptions);
675                self
676            }
677
678            /// Enable the Pub/Sub push channel without configuring any connect-time
679            /// subscriptions, so the client can use runtime `subscribe`/`unsubscribe`
680            /// and receive messages via `get_pubsub_message`. Connect-time
681            /// `subscriptions` enable this implicitly.
682            ///
683            /// Note: runtime subscriptions are session-scoped — unlike connect-time
684            /// subscriptions they are not automatically restored after a reconnect.
685            #[must_use]
686            pub fn enable_pubsub(mut self) -> Self {
687                self.force_pubsub_channel = true;
688                self
689            }
690
691            /// Lower the fields shared by both configurations into a
692            /// `ConnectionRequest`. Mode-specific fields (`cluster_mode_enabled`,
693            /// `database_id`, `periodic_checks`) are layered on by `to_request()`.
694            pub(crate) fn common_request(&self) -> glide_core::client::ConnectionRequest {
695                use glide_core::client::ConnectionRequest;
696                // TODO #7162: build via a glide-core ConnectionRequest builder with
697                // intended defaults, instead of a struct literal + `..default()` that
698                // silently absorbs new fields (see the tcp_nodelay note below).
699                let mut req = ConnectionRequest {
700                    addresses: self.addresses.iter().map(NodeAddress::to_core).collect(),
701                    tls_mode: Some(self.tls.to_core()),
702                    read_from: Some(self.read_from.to_core()),
703                    protocol: Some(self.protocol.to_core()),
704                    client_name: self.client_name.clone(),
705                    lib_name: Some($crate::config::common::LIB_NAME.to_string()),
706                    lib_ver: Some($crate::config::common::LIB_VERSION.to_string()),
707                    lazy_connect: self.lazy_connect,
708                    inflight_requests_limit: self.inflight_requests_limit,
709                    // Disable Nagle's algorithm. We build `ConnectionRequest`
710                    // directly, so we do NOT inherit glide-core's protobuf-path
711                    // default of `tcp_nodelay = true` (the bare struct `Default` is
712                    // `false`). Leaving Nagle on interacts with delayed-ACK to add
713                    // multi-ms tail latency under high concurrency, so we
714                    // explicitly enable TCP_NODELAY to match the intended core
715                    // default.
716                    tcp_nodelay: true,
717                    // All other fields (periodic_checks, database_id,
718                    // pubsub_subscriptions, cluster_mode_enabled, tls certs, otel,
719                    // IAM, ...) are intentionally left at their core defaults here
720                    // and set by the caller's `to_request()`. If glide-core adds a
721                    // field this default absorbs it silently — revisit when bumping
722                    // the glide-core dependency.
723                    ..ConnectionRequest::default()
724                };
725
726                if !self.root_certs.is_empty() {
727                    req.root_certs = self.root_certs.clone();
728                }
729                if let Some(identity) = &self.client_identity {
730                    req.client_cert = identity.cert_pem().to_vec();
731                    req.client_key = identity.key_pem().to_vec();
732                }
733                if let Some(subs) = &self.pubsub_subscriptions {
734                    req.pubsub_subscriptions = Some(subs.to_core());
735                }
736                if let Some(creds) = &self.credentials {
737                    req.authentication_info = Some(creds.to_core());
738                }
739                if let Some(t) = self.request_timeout {
740                    req.request_timeout = Some(duration_as_millis_u32(t));
741                }
742                if let Some(t) = self.connection_timeout {
743                    req.connection_timeout = Some(duration_as_millis_u32(t));
744                }
745                if let Some(strategy) = self.reconnect_strategy {
746                    req.connection_retry_strategy = Some(strategy.to_core());
747                }
748                req
749            }
750        }
751    };
752}
753
754pub(crate) use impl_common_config_builders;