Skip to main content

praxis_core/connectivity/
connection_options.rs

1// SPDX-License-Identifier: LGPL-3.0-only
2// Copyright (c) 2024 Shane Utt
3
4//! Per-upstream connection tuning (timeouts).
5//!
6//! Derived from cluster-level timeout settings in the config.
7
8use std::time::Duration;
9
10use crate::config::Cluster;
11
12// -----------------------------------------------------------------------------
13// ConnectionOptions
14// -----------------------------------------------------------------------------
15
16/// Per-upstream connection tuning (timeouts, pool settings).
17///
18/// ```
19/// use std::time::Duration;
20///
21/// use praxis_core::connectivity::ConnectionOptions;
22///
23/// let opts = ConnectionOptions {
24///     connection_timeout: Some(Duration::from_secs(5)),
25///     ..Default::default()
26/// };
27///
28/// assert_eq!(opts.connection_timeout, Some(Duration::from_secs(5)));
29/// assert!(opts.idle_timeout.is_none());
30/// ```
31#[derive(Debug, Clone, Default)]
32pub struct ConnectionOptions {
33    /// TCP connection timeout.
34    pub connection_timeout: Option<Duration>,
35
36    /// Idle connection timeout.
37    pub idle_timeout: Option<Duration>,
38
39    /// Read timeout.
40    pub read_timeout: Option<Duration>,
41
42    /// Total connection timeout (TCP connect + TLS handshake).
43    pub total_connection_timeout: Option<Duration>,
44
45    /// Write timeout.
46    pub write_timeout: Option<Duration>,
47}
48
49/// Converts cluster timeout fields (milliseconds) to [`Duration`] values.
50///
51/// ```
52/// use std::time::Duration;
53///
54/// use praxis_core::{config::Cluster, connectivity::ConnectionOptions};
55///
56/// let cluster: Cluster = serde_yaml::from_str(
57///     r#"
58/// name: backend
59/// endpoints: ["10.0.0.1:80"]
60/// connection_timeout_ms: 5000
61/// "#,
62/// )
63/// .unwrap();
64/// let opts = ConnectionOptions::from(&cluster);
65/// assert_eq!(opts.connection_timeout, Some(Duration::from_secs(5)));
66/// ```
67impl From<&Cluster> for ConnectionOptions {
68    fn from(cluster: &Cluster) -> Self {
69        Self {
70            connection_timeout: cluster.connection_timeout_ms.map(Duration::from_millis),
71            idle_timeout: cluster.idle_timeout_ms.map(Duration::from_millis),
72            read_timeout: cluster.read_timeout_ms.map(Duration::from_millis),
73            total_connection_timeout: cluster.total_connection_timeout_ms.map(Duration::from_millis),
74            write_timeout: cluster.write_timeout_ms.map(Duration::from_millis),
75        }
76    }
77}
78
79// -----------------------------------------------------------------------------
80// Tests
81// -----------------------------------------------------------------------------
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn default_is_all_none() {
89        let opts = ConnectionOptions::default();
90        assert!(
91            opts.connection_timeout.is_none(),
92            "connection_timeout should default to None"
93        );
94        assert!(opts.idle_timeout.is_none(), "idle_timeout should default to None");
95        assert!(opts.read_timeout.is_none(), "read_timeout should default to None");
96        assert!(
97            opts.total_connection_timeout.is_none(),
98            "total_connection_timeout should default to None"
99        );
100        assert!(opts.write_timeout.is_none(), "write_timeout should default to None");
101    }
102
103    #[test]
104    fn from_cluster_maps_millis_to_duration() {
105        let c = cluster(Some(1000), Some(2000), Some(3000), Some(4000), Some(5000));
106        let opts = ConnectionOptions::from(&c);
107        assert_eq!(
108            opts.connection_timeout,
109            Some(Duration::from_millis(1000)),
110            "connection_timeout mismatch"
111        );
112        assert_eq!(
113            opts.idle_timeout,
114            Some(Duration::from_millis(2000)),
115            "idle_timeout mismatch"
116        );
117        assert_eq!(
118            opts.read_timeout,
119            Some(Duration::from_millis(3000)),
120            "read_timeout mismatch"
121        );
122        assert_eq!(
123            opts.total_connection_timeout,
124            Some(Duration::from_millis(5000)),
125            "total_connection_timeout mismatch"
126        );
127        assert_eq!(
128            opts.write_timeout,
129            Some(Duration::from_millis(4000)),
130            "write_timeout mismatch"
131        );
132    }
133
134    #[test]
135    fn from_cluster_preserves_none_fields() {
136        let c = cluster(Some(500), None, None, None, None);
137        let opts = ConnectionOptions::from(&c);
138        assert_eq!(
139            opts.connection_timeout,
140            Some(Duration::from_millis(500)),
141            "connection_timeout should be set"
142        );
143        assert!(opts.idle_timeout.is_none(), "idle_timeout should remain None");
144        assert!(opts.read_timeout.is_none(), "read_timeout should remain None");
145        assert!(
146            opts.total_connection_timeout.is_none(),
147            "total_connection_timeout should remain None"
148        );
149        assert!(opts.write_timeout.is_none(), "write_timeout should remain None");
150    }
151
152    #[test]
153    fn from_cluster_all_none() {
154        let c = cluster(None, None, None, None, None);
155        let opts = ConnectionOptions::from(&c);
156        assert!(opts.connection_timeout.is_none(), "connection_timeout should be None");
157        assert!(opts.idle_timeout.is_none(), "idle_timeout should be None");
158        assert!(opts.read_timeout.is_none(), "read_timeout should be None");
159        assert!(
160            opts.total_connection_timeout.is_none(),
161            "total_connection_timeout should be None"
162        );
163        assert!(opts.write_timeout.is_none(), "write_timeout should be None");
164    }
165
166    // -------------------------------------------------------------------------
167    // Test Utilities
168    // -------------------------------------------------------------------------
169
170    /// Build a [`Cluster`] with custom timeout fields for testing.
171    fn cluster(
172        connection_timeout_ms: Option<u64>,
173        idle_timeout_ms: Option<u64>,
174        read_timeout_ms: Option<u64>,
175        write_timeout_ms: Option<u64>,
176        total_connection_timeout_ms: Option<u64>,
177    ) -> Cluster {
178        Cluster {
179            connection_timeout_ms,
180            idle_timeout_ms,
181            read_timeout_ms,
182            total_connection_timeout_ms,
183            write_timeout_ms,
184            ..Cluster::with_defaults("test", vec![])
185        }
186    }
187}