tact_client/
pool.rs

1//! HTTP connection pool manager for TACT clients
2
3use reqwest::Client;
4use std::sync::OnceLock;
5use std::time::Duration;
6use tracing::debug;
7
8/// Global connection pool for TACT HTTP clients
9static GLOBAL_POOL: OnceLock<Client> = OnceLock::new();
10
11/// Default connection pool settings optimized for TACT operations
12const DEFAULT_MAX_IDLE_CONNECTIONS: usize = 100;
13const DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST: usize = 32;
14const DEFAULT_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
15const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
16const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
17
18/// Configuration for the HTTP connection pool
19#[derive(Debug, Clone)]
20pub struct PoolConfig {
21    /// Maximum idle connections in the pool
22    pub max_idle_connections: Option<usize>,
23    /// Maximum idle connections per host
24    pub max_idle_connections_per_host: usize,
25    /// How long to keep idle connections alive
26    pub pool_idle_timeout: Duration,
27    /// Request timeout
28    pub request_timeout: Duration,
29    /// Connection timeout
30    pub connect_timeout: Duration,
31    /// User agent string
32    pub user_agent: Option<String>,
33}
34
35impl Default for PoolConfig {
36    fn default() -> Self {
37        Self {
38            max_idle_connections: Some(DEFAULT_MAX_IDLE_CONNECTIONS),
39            max_idle_connections_per_host: DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST,
40            pool_idle_timeout: Duration::from_secs(DEFAULT_POOL_IDLE_TIMEOUT_SECS),
41            request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS),
42            connect_timeout: Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS),
43            user_agent: None,
44        }
45    }
46}
47
48impl PoolConfig {
49    /// Create a new pool configuration
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Set maximum idle connections
55    pub fn with_max_idle_connections(mut self, max: Option<usize>) -> Self {
56        self.max_idle_connections = max;
57        self
58    }
59
60    /// Set maximum idle connections per host
61    pub fn with_max_idle_connections_per_host(mut self, max: usize) -> Self {
62        self.max_idle_connections_per_host = max;
63        self
64    }
65
66    /// Set pool idle timeout
67    pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
68        self.pool_idle_timeout = timeout;
69        self
70    }
71
72    /// Set request timeout
73    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
74        self.request_timeout = timeout;
75        self
76    }
77
78    /// Set connection timeout
79    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
80        self.connect_timeout = timeout;
81        self
82    }
83
84    /// Set user agent
85    pub fn with_user_agent(mut self, user_agent: String) -> Self {
86        self.user_agent = Some(user_agent);
87        self
88    }
89}
90
91/// Initialize the global connection pool with custom configuration
92///
93/// This should be called once at application startup. If called multiple times,
94/// subsequent calls will be ignored and the original pool configuration will be used.
95///
96/// Returns whether the pool was successfully initialized (true) or was already
97/// initialized (false).
98pub fn init_global_pool(config: PoolConfig) -> bool {
99    let client = create_pooled_client(config);
100
101    match GLOBAL_POOL.set(client) {
102        Ok(()) => {
103            debug!("Initialized global TACT HTTP connection pool");
104            true
105        }
106        Err(_) => {
107            debug!("Global TACT HTTP connection pool already initialized");
108            false
109        }
110    }
111}
112
113/// Get the global connection pool
114///
115/// If the pool hasn't been initialized with `init_global_pool()`, this will
116/// initialize it with default settings.
117pub fn get_global_pool() -> &'static Client {
118    GLOBAL_POOL.get_or_init(|| {
119        debug!("Creating default global TACT HTTP connection pool");
120        create_pooled_client(PoolConfig::default())
121    })
122}
123
124/// Create a new pooled HTTP client with the specified configuration
125pub fn create_pooled_client(config: PoolConfig) -> Client {
126    debug!(
127        "Creating HTTP client with pool settings: max_idle={:?}, max_per_host={}, idle_timeout={:?}",
128        config.max_idle_connections, config.max_idle_connections_per_host, config.pool_idle_timeout
129    );
130
131    let mut builder = Client::builder()
132        .pool_max_idle_per_host(config.max_idle_connections_per_host)
133        .pool_idle_timeout(config.pool_idle_timeout)
134        .timeout(config.request_timeout)
135        .connect_timeout(config.connect_timeout)
136        .use_rustls_tls() // Use rustls for TLS (more predictable than native-tls)
137        // HTTP/2 is automatically negotiated when available
138        .tcp_keepalive(Duration::from_secs(60)); // Keep TCP connections alive
139
140    if let Some(max_idle) = config.max_idle_connections {
141        builder = builder.pool_max_idle_per_host(max_idle);
142    }
143
144    if let Some(user_agent) = config.user_agent {
145        builder = builder.user_agent(user_agent);
146    }
147
148    builder.build().expect("Failed to create HTTP client")
149}
150
151/// Get pool statistics (if available from reqwest)
152///
153/// Note: reqwest doesn't currently expose detailed connection pool metrics,
154/// but this function is provided for future compatibility.
155pub fn get_pool_stats() -> PoolStats {
156    // reqwest doesn't expose pool metrics yet, so we return empty stats
157    PoolStats {
158        active_connections: None,
159        idle_connections: None,
160        total_connections: None,
161    }
162}
163
164/// Connection pool statistics
165#[derive(Debug, Clone)]
166pub struct PoolStats {
167    /// Number of active connections (if available)
168    pub active_connections: Option<usize>,
169    /// Number of idle connections (if available)
170    pub idle_connections: Option<usize>,
171    /// Total number of connections (if available)
172    pub total_connections: Option<usize>,
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_pool_config_builder() {
181        let config = PoolConfig::new()
182            .with_max_idle_connections(Some(50))
183            .with_max_idle_connections_per_host(20)
184            .with_pool_idle_timeout(Duration::from_secs(60))
185            .with_request_timeout(Duration::from_secs(45))
186            .with_connect_timeout(Duration::from_secs(15))
187            .with_user_agent("Test/1.0".to_string());
188
189        assert_eq!(config.max_idle_connections, Some(50));
190        assert_eq!(config.max_idle_connections_per_host, 20);
191        assert_eq!(config.pool_idle_timeout, Duration::from_secs(60));
192        assert_eq!(config.request_timeout, Duration::from_secs(45));
193        assert_eq!(config.connect_timeout, Duration::from_secs(15));
194        assert_eq!(config.user_agent, Some("Test/1.0".to_string()));
195    }
196
197    #[test]
198    fn test_create_pooled_client() {
199        let config = PoolConfig::default();
200        let client = create_pooled_client(config);
201
202        // Just verify the client was created successfully
203        assert!(std::ptr::addr_of!(client) as usize != 0);
204    }
205
206    #[test]
207    fn test_global_pool_initialization() {
208        // Note: This test may interfere with other tests that use the global pool
209        // In practice, the global pool should be initialized once per application
210        let _config = PoolConfig::default().with_user_agent("TestPool/1.0".to_string());
211
212        // Since we can't easily reset the global pool, we just test that getting it works
213        let _pool = get_global_pool();
214
215        // Verify pool stats function doesn't panic
216        let _stats = get_pool_stats();
217    }
218
219    #[test]
220    fn test_pool_config_defaults() {
221        let config = PoolConfig::default();
222        assert_eq!(
223            config.max_idle_connections,
224            Some(DEFAULT_MAX_IDLE_CONNECTIONS)
225        );
226        assert_eq!(
227            config.max_idle_connections_per_host,
228            DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST
229        );
230        assert_eq!(
231            config.pool_idle_timeout,
232            Duration::from_secs(DEFAULT_POOL_IDLE_TIMEOUT_SECS)
233        );
234        assert_eq!(
235            config.request_timeout,
236            Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS)
237        );
238        assert_eq!(
239            config.connect_timeout,
240            Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS)
241        );
242        assert!(config.user_agent.is_none());
243    }
244}