Skip to main content

uri_register/
postgres.rs

1// Copyright TELICENT LTD
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::cache::{create_cache, Cache, CacheStrategy};
16use crate::error::{ConfigurationError, Result};
17use crate::service::UriService;
18use async_trait::async_trait;
19use deadpool_postgres::{ManagerConfig, Pool, RecyclingMethod, Runtime};
20use rustls::RootCertStore;
21use rustls_pki_types::pem::PemObject;
22use std::sync::Arc;
23use tokio_postgres::{Config, NoTls};
24use tokio_postgres_rustls::MakeRustlsConnect;
25use tracing::{debug, info, instrument, trace, warn};
26use url::Url;
27
28/// PostgreSQL-based URI register implementation with configurable caching
29///
30/// This implementation uses a PostgreSQL table to store URI-to-ID mappings
31/// with an in-memory cache (W-TinyLFU by default, or LRU) to reduce database round-trips.
32/// It's designed for high concurrency with connection pooling and batch operations.
33///
34/// ## Prerequisites
35///
36/// The database schema must be initialized before using this service.
37/// See `schema.sql` for the DDL statements.
38///
39/// ## URI Validation
40///
41/// All URIs are validated before registration to ensure they conform to RFC 3986.
42/// Invalid URIs will return an error.
43///
44/// ## Performance
45///
46/// With default logged tables on typical hardware:
47/// - Batch insert: ~10K-50K URIs/sec
48/// - Batch lookup (cached): ~100K-1M+ URIs/sec (no DB round-trip)
49/// - Batch lookup (uncached): ~100K-200K URIs/sec
50/// - Query overhead: ~2-10ms per query (2 round-trips)
51///
52/// The cache (W-TinyLFU or LRU) significantly improves performance for repeated URI lookups.
53/// Cache strategy and size are configurable when creating the register instance.
54///
55/// For faster writes at the cost of durability, the table can be configured
56/// as UNLOGGED (see `schema.sql` for options).
57pub struct PostgresUriRegister {
58    pool: Pool,
59    /// Cache for URI-to-ID mappings (W-TinyLFU or LRU)
60    cache: Arc<dyn Cache>,
61    /// Name of the database table to use
62    table_name: String,
63}
64
65impl PostgresUriRegister {
66    /// Create a new PostgreSQL URI register service with configurable cache
67    ///
68    /// # Arguments
69    ///
70    /// * `database_url` - PostgreSQL connection string (e.g., "postgres://user:password@host:port/database")
71    /// * `table_name` - Name of the database table to use (must be a valid SQL identifier, default: "uri_register")
72    /// * `max_connections` - Maximum number of connections in the pool (recommended: 10-50)
73    /// * `cache_size` - Number of URI-to-ID mappings to cache in memory (recommended: 1,000-100,000)
74    /// * `cache_strategy` - Cache strategy to use (Moka/W-TinyLFU is default and recommended for most workloads)
75    ///
76    /// # Prerequisites
77    ///
78    /// The database schema must be initialized before using this service.
79    /// See the `schema.sql` file and README.md for setup instructions.
80    ///
81    /// # Example
82    ///
83    /// ```rust,no_run
84    /// use uri_register::PostgresUriRegister;
85    ///
86    /// #[tokio::main]
87    /// async fn main() -> uri_register::Result<()> {
88    ///     let register = PostgresUriRegister::new(
89    ///         "postgres://localhost/mydb",
90    ///         "uri_register",  // table name
91    ///         20,              // max connections
92    ///         10_000           // cache size (defaults to Moka/W-TinyLFU)
93    ///     ).await?;
94    ///     Ok(())
95    /// }
96    /// ```
97    pub async fn new(
98        database_url: &str,
99        table_name: &str,
100        max_connections: u32,
101        cache_size: usize,
102    ) -> Result<Self> {
103        Self::new_with_cache_strategy(
104            database_url,
105            table_name,
106            max_connections,
107            cache_size,
108            None, // Default to Moka
109            None, // Default to no TLS
110            None, // No custom CA cert
111        )
112        .await
113    }
114
115    /// Create a new PostgreSQL URI register with a specific cache strategy and TLS
116    ///
117    /// This is identical to `new()` but allows specifying a cache strategy and TLS option.
118    /// Most users should use `new()` which defaults to the recommended Moka (W-TinyLFU) cache and no TLS.
119    ///
120    /// # Arguments
121    ///
122    /// * `cache_strategy` - Optional cache strategy (None = Moka default, or specify CacheStrategy::Lru)
123    /// * `use_tls` - Optional TLS flag (None/false = no TLS, true = TLS with webpki root certificates)
124    /// * `ca_cert_path` - Optional path to a PEM-encoded CA certificate file for verifying
125    ///   connections to servers using certificates signed by a private/internal CA.
126    ///   When provided, `use_tls` is automatically enabled.
127    ///
128    /// # Example
129    ///
130    /// ```rust,no_run
131    /// use uri_register::{CacheStrategy, PostgresUriRegister};
132    ///
133    /// #[tokio::main]
134    /// async fn main() -> uri_register::Result<()> {
135    ///     // Use LRU instead of default Moka, with TLS enabled
136    ///     let register = PostgresUriRegister::new_with_cache_strategy(
137    ///         "postgres://localhost/mydb",
138    ///         "uri_register",
139    ///         20,
140    ///         10_000,
141    ///         Some(CacheStrategy::Lru),
142    ///         Some(true),  // Enable TLS
143    ///         None,        // No custom CA cert
144    ///     ).await?;
145    ///     Ok(())
146    /// }
147    /// ```
148    pub async fn new_with_cache_strategy(
149        database_url: &str,
150        table_name: &str,
151        max_connections: u32,
152        cache_size: usize,
153        cache_strategy: Option<CacheStrategy>,
154        use_tls: Option<bool>,
155        ca_cert_path: Option<&str>,
156    ) -> Result<Self> {
157        // Validate inputs
158        if cache_size == 0 {
159            return Err(ConfigurationError::InvalidCacheSize(cache_size).into());
160        }
161
162        if max_connections == 0 {
163            return Err(ConfigurationError::InvalidMaxConnections(max_connections).into());
164        }
165
166        // Validate table name as SQL identifier
167        Self::validate_table_name(table_name)?;
168
169        // Parse the database URL into tokio-postgres Config
170        let pg_config: Config = database_url.parse().map_err(|e| {
171            ConfigurationError::InvalidBackoff(format!("Failed to parse database URL: {}", e))
172        })?;
173
174        // Create deadpool configuration
175        let mut cfg = deadpool_postgres::Config::new();
176        cfg.dbname = pg_config.get_dbname().map(|s| s.to_string());
177        cfg.host = pg_config.get_hosts().first().map(|h| match h {
178            tokio_postgres::config::Host::Tcp(s) => s.to_string(),
179            #[cfg(unix)]
180            tokio_postgres::config::Host::Unix(p) => p.to_str().unwrap_or_default().to_string(),
181        });
182        cfg.port = pg_config.get_ports().first().copied();
183        cfg.user = pg_config.get_user().map(|s| s.to_string());
184        cfg.password = pg_config
185            .get_password()
186            .map(|p| std::str::from_utf8(p).unwrap_or_default().to_string());
187        cfg.manager = Some(ManagerConfig {
188            recycling_method: RecyclingMethod::Fast,
189        });
190        cfg.pool = Some(deadpool_postgres::PoolConfig {
191            max_size: max_connections as usize,
192            timeouts: deadpool_postgres::Timeouts {
193                wait: Some(std::time::Duration::from_secs(10)),
194                create: Some(std::time::Duration::from_secs(10)),
195                recycle: Some(std::time::Duration::from_secs(10)),
196            },
197            ..Default::default()
198        });
199
200        // If a CA cert path is provided, TLS is implicitly enabled
201        let effective_tls = use_tls.unwrap_or(false) || ca_cert_path.is_some();
202
203        // Security logging for connection configuration
204        if !effective_tls {
205            warn!(
206                "TLS is DISABLED for database connection - data will be transmitted in plaintext. \
207                 This is not recommended for production environments."
208            );
209        }
210
211        if let Some(password) = pg_config.get_password() {
212            if password.is_empty() {
213                warn!("Database connection configured with an empty password.");
214            }
215        } else {
216            warn!("Database connection configured without a password.");
217        }
218
219        let pool = if effective_tls {
220            let mut root_store = RootCertStore::empty();
221
222            // Always include the public webpki root certificates
223            root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
224
225            // Load custom CA certificate if provided (for private/internal CAs)
226            if let Some(cert_path) = ca_cert_path {
227                info!(
228                    ca_cert_path = cert_path,
229                    "Loading custom CA certificate for TLS verification"
230                );
231
232                let pem_data = std::fs::read(cert_path).map_err(|e| {
233                    ConfigurationError::InvalidBackoff(format!(
234                        "Failed to read CA certificate file '{}': {}",
235                        cert_path, e
236                    ))
237                })?;
238
239                let certs: Vec<_> = rustls_pki_types::CertificateDer::pem_slice_iter(&pem_data)
240                    .collect::<std::result::Result<Vec<_>, _>>()
241                    .map_err(|e| {
242                        ConfigurationError::InvalidBackoff(format!(
243                            "Failed to parse PEM certificates from '{}': {}",
244                            cert_path, e
245                        ))
246                    })?;
247
248                if certs.is_empty() {
249                    return Err(ConfigurationError::InvalidBackoff(format!(
250                        "No valid certificates found in CA certificate file '{}'",
251                        cert_path
252                    ))
253                    .into());
254                }
255
256                warn!(
257                    cert_count = certs.len(),
258                    ca_cert_path = cert_path,
259                    "Custom CA certificate(s) loaded - connections will trust certificates signed \
260                     by this CA in addition to public CAs. Ensure this CA certificate is from a \
261                     trusted source."
262                );
263
264                let (added, _ignored) = root_store.add_parsable_certificates(certs);
265                if added == 0 {
266                    return Err(ConfigurationError::InvalidBackoff(format!(
267                        "None of the certificates in '{}' could be added to the trust store",
268                        cert_path
269                    ))
270                    .into());
271                }
272                info!(
273                    added_certs = added,
274                    "Custom CA certificates added to trust store"
275                );
276            }
277
278            let tls_config = rustls::ClientConfig::builder()
279                .with_root_certificates(root_store)
280                .with_no_client_auth();
281
282            let tls = MakeRustlsConnect::new(tls_config);
283
284            cfg.create_pool(Some(Runtime::Tokio1), tls).map_err(|e| {
285                ConfigurationError::InvalidBackoff(format!(
286                    "Failed to create connection pool with TLS: {}",
287                    e
288                ))
289            })?
290        } else {
291            cfg.create_pool(Some(Runtime::Tokio1), NoTls).map_err(|e| {
292                ConfigurationError::InvalidBackoff(format!(
293                    "Failed to create connection pool: {}",
294                    e
295                ))
296            })?
297        };
298
299        let cache = create_cache(cache_strategy.unwrap_or_default(), cache_size);
300
301        info!(
302            table = table_name,
303            max_connections,
304            cache_size,
305            tls = effective_tls,
306            custom_ca = ca_cert_path.is_some(),
307            "URI register connected"
308        );
309
310        Ok(Self {
311            pool,
312            cache,
313            table_name: table_name.to_string(),
314        })
315    }
316
317    /// Validate that a table name is a valid SQL identifier
318    ///
319    /// Prevents SQL injection by ensuring the table name only contains
320    /// alphanumeric characters and underscores, and doesn't start with a digit.
321    fn validate_table_name(name: &str) -> Result<()> {
322        if name.is_empty() {
323            return Err(ConfigurationError::InvalidTableName(
324                "table name cannot be empty".to_string(),
325            )
326            .into());
327        }
328
329        if name.len() > 63 {
330            return Err(ConfigurationError::InvalidTableName(format!(
331                "table name too long (max 63 characters): '{}'",
332                name
333            ))
334            .into());
335        }
336
337        // First character must be a letter or underscore
338        let first_char = name.chars().next().unwrap();
339        if !first_char.is_ascii_alphabetic() && first_char != '_' {
340            return Err(ConfigurationError::InvalidTableName(format!(
341                "table name must start with a letter or underscore: '{}'",
342                name
343            ))
344            .into());
345        }
346
347        // All characters must be alphanumeric or underscore
348        if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
349            return Err(ConfigurationError::InvalidTableName(format!(
350                "table name can only contain letters, numbers, and underscores: '{}'",
351                name
352            ))
353            .into());
354        }
355
356        Ok(())
357    }
358
359    /// Get statistics about the URI register
360    ///
361    /// Returns the total number of URIs and the storage size.
362    ///
363    /// # Example
364    ///
365    /// ```rust,no_run
366    /// use uri_register::PostgresUriRegister;
367    ///
368    /// #[tokio::main]
369    /// async fn main() -> uri_register::Result<()> {
370    ///     let register = PostgresUriRegister::new(
371    ///         "postgres://localhost/mydb",
372    ///         "uri_register",
373    ///         20,
374    ///         10_000
375    ///     ).await?;
376    ///     let stats = register.stats().await?;
377    ///     println!("Total URIs: {}", stats.total_uris);
378    ///     println!("Size: {} bytes", stats.size_bytes);
379    ///     Ok(())
380    /// }
381    /// ```
382    pub async fn stats(&self) -> Result<RegisterStats> {
383        // Build query with validated table name (safe from SQL injection)
384        let query = format!(
385            r#"
386            SELECT
387                COUNT(*)::bigint as count,
388                pg_total_relation_size('{}')::bigint as size_bytes
389            FROM {}
390            "#,
391            self.table_name, self.table_name
392        );
393
394        // Execute with retry logic
395        let client = self.pool.get().await.map_err(|e| {
396            crate::error::Error::Database(format!("Failed to get database connection: {}", e))
397        })?;
398
399        let rows = client
400            .query(&query, &[])
401            .await
402            .map_err(|e| crate::error::Error::Database(e.to_string()))?;
403
404        let row = rows.into_iter().next().ok_or_else(|| {
405            crate::error::Error::Database("No rows returned from stats query".to_string())
406        })?;
407
408        // Get cache statistics
409        let cache_stats = self.cache.stats();
410
411        // Get connection pool statistics
412        let status = self.pool.status();
413        let pool_stats = PoolStats {
414            connections_active: (status.size - status.available) as u32,
415            connections_idle: status.available as u32,
416            connections_max: status.max_size as u32,
417        };
418
419        Ok(RegisterStats {
420            total_uris: row.get::<_, i64>("count") as u64,
421            size_bytes: row.get::<_, i64>("size_bytes") as u64,
422            cache: cache_stats,
423            pool: pool_stats,
424        })
425    }
426
427    /// Clone the register instance (shares pool and cache)
428    ///
429    /// This is a shallow clone that shares both the connection pool and cache.
430    /// Both pool and cache use Arc internally, so this clone is cheap and shares
431    /// the underlying resources.
432    ///
433    /// This method is primarily used for Python bindings where we need to move
434    /// data into async closures.
435    #[cfg(feature = "python")]
436    pub(crate) fn clone_inner(&self) -> Self {
437        PostgresUriRegister {
438            pool: self.pool.clone(),
439            cache: self.cache.clone(), // Clone the Arc, shares the same cache
440            table_name: self.table_name.clone(),
441        }
442    }
443
444    /// Validate that a string is a valid URI according to RFC 3986
445    fn validate_uri(uri: &str) -> Result<()> {
446        Url::parse(uri).map_err(|e| {
447            crate::error::Error::InvalidUri(format!("Invalid URI '{}': {}", uri, e))
448        })?;
449        Ok(())
450    }
451}
452
453#[async_trait]
454impl UriService for PostgresUriRegister {
455    #[instrument(skip(self), fields(table = %self.table_name))]
456    async fn register_uri(&self, uri: &str) -> Result<u64> {
457        // Validate URI first
458        Self::validate_uri(uri)?;
459
460        // Check cache first
461        if let Some(id) = self.cache.get(uri) {
462            trace!(id, "cache hit");
463            return Ok(id);
464        }
465        trace!("cache miss, querying database");
466
467        // Insert and return ID (ON CONFLICT handles race conditions and existing URIs)
468        // Build query with validated table name (safe from SQL injection)
469        let query = format!(
470            r#"
471            INSERT INTO {} (uri)
472            VALUES ($1)
473            ON CONFLICT (uri_hash) DO UPDATE SET uri = EXCLUDED.uri
474            RETURNING id
475            "#,
476            self.table_name
477        );
478
479        // Execute with retry logic
480        let client = self.pool.get().await.map_err(|e| {
481            crate::error::Error::Database(format!("Failed to get database connection: {}", e))
482        })?;
483
484        let rows = client
485            .query(&query, &[&uri])
486            .await
487            .map_err(|e| crate::error::Error::Database(e.to_string()))?;
488
489        let result = rows.into_iter().next().ok_or_else(|| {
490            crate::error::Error::Database("No rows returned from register_uri query".to_string())
491        })?;
492
493        let id = result.get::<_, i64>("id") as u64;
494
495        // Update cache
496        self.cache.put(uri.to_string(), id);
497
498        Ok(id)
499    }
500
501    #[instrument(skip(self, uris), fields(table = %self.table_name, batch_size = uris.len()))]
502    async fn register_uri_batch(&self, uris: &[String]) -> Result<Vec<u64>> {
503        if uris.is_empty() {
504            trace!("empty batch, returning early");
505            return Ok(Vec::new());
506        }
507
508        // Validate all URIs first
509        for uri in uris {
510            Self::validate_uri(uri)?;
511        }
512
513        // CORRECTNESS GUARANTEE: Order preservation
514        // We maintain strict correspondence between input URIs and output IDs
515        // by tracking the original index of each URI and using URI strings
516        // (not SQL result order) to map IDs back to their positions.
517
518        let mut result_ids = vec![None; uris.len()];
519        let mut uncached_indices = Vec::new();
520        let mut uncached_uris_dedup = Vec::new();
521        let mut seen_uncached = std::collections::HashMap::new();
522
523        // Step 1: Check cache for all URIs
524        for (idx, uri) in uris.iter().enumerate() {
525            if let Some(id) = self.cache.get(uri) {
526                result_ids[idx] = Some(id);
527            } else {
528                uncached_indices.push(idx);
529                // Deduplicate uncached URIs for DB query
530                if !seen_uncached.contains_key(uri) {
531                    seen_uncached.insert(uri.clone(), uncached_uris_dedup.len());
532                    uncached_uris_dedup.push(uri.clone());
533                }
534            }
535        }
536
537        // If everything was cached, return early
538        if uncached_uris_dedup.is_empty() {
539            debug!(cached = uris.len(), "all URIs found in cache");
540            return Ok(result_ids.into_iter().map(|id| id.unwrap()).collect());
541        }
542
543        let cached_count = uris.len() - uncached_indices.len();
544        debug!(
545            cached = cached_count,
546            uncached = uncached_uris_dedup.len(),
547            "cache lookup complete, querying database"
548        );
549
550        // Step 2: Register deduplicated uncached URIs in batch
551        // IMPORTANT: SQL may return results in ANY order (not guaranteed to match input order)
552        // We use "RETURNING id, uri" to get BOTH values together, then map by URI string
553        // Build query with validated table name (safe from SQL injection)
554        let query = format!(
555            r#"
556            INSERT INTO {} (uri)
557            SELECT unnest($1::text[])
558            ON CONFLICT (uri_hash) DO UPDATE SET uri = EXCLUDED.uri
559            RETURNING id, uri
560            "#,
561            self.table_name
562        );
563
564        // Execute with retry logic
565        let client = self.pool.get().await.map_err(|e| {
566            crate::error::Error::Database(format!("Failed to get database connection: {}", e))
567        })?;
568
569        let rows = client
570            .query(&query, &[&uncached_uris_dedup])
571            .await
572            .map_err(|e| crate::error::Error::Database(e.to_string()))?;
573
574        // Build a map of URI -> ID from database results
575        // This allows us to look up IDs by URI string (order-independent)
576        let mut uri_to_id = std::collections::HashMap::new();
577        for row in rows {
578            let uri: String = row.get("uri");
579            let id: i64 = row.get("id");
580            uri_to_id.insert(uri, id as u64);
581        }
582
583        // Step 3: Fill in the result vector and update cache
584        // CORRECTNESS: We use the saved indices and look up by URI string,
585        // guaranteeing that result_ids[i] corresponds to uris[i]
586        for idx in uncached_indices {
587            let uri = &uris[idx]; // Get URI from original position
588            if let Some(&id) = uri_to_id.get(uri) {
589                // Look up ID by URI string
590                result_ids[idx] = Some(id); // Store at original index
591                self.cache.put(uri.clone(), id);
592            }
593        }
594
595        // Convert Option<u64> to u64 (all should be Some at this point)
596        Ok(result_ids
597            .into_iter()
598            .map(|id| id.expect("All URIs should have IDs"))
599            .collect())
600    }
601
602    #[instrument(skip(self, uris), fields(table = %self.table_name, batch_size = uris.len()))]
603    async fn register_uri_batch_hashmap(
604        &self,
605        uris: &[String],
606    ) -> Result<std::collections::HashMap<String, u64>> {
607        if uris.is_empty() {
608            trace!("empty batch, returning early");
609            return Ok(std::collections::HashMap::new());
610        }
611
612        // Validate all URIs first
613        for uri in uris {
614            Self::validate_uri(uri)?;
615        }
616
617        // CORRECTNESS GUARANTEE: URI-to-ID mapping accuracy
618        // Each URI in the result HashMap is guaranteed to map to its correct ID
619        // because SQL returns both 'id' and 'uri' together in each row (RETURNING id, uri).
620        // We never rely on positional correspondence, eliminating ordering errors.
621
622        let mut result = std::collections::HashMap::new();
623        let mut uncached_uris = Vec::new();
624
625        // Step 1: Deduplicate input and check cache
626        let unique_uris: std::collections::HashSet<_> = uris.iter().collect();
627
628        for uri in unique_uris {
629            if let Some(id) = self.cache.get(uri) {
630                result.insert(uri.clone(), id);
631            } else {
632                uncached_uris.push(uri.clone());
633            }
634        }
635
636        // If everything was cached, return early
637        if uncached_uris.is_empty() {
638            debug!(cached = result.len(), "all URIs found in cache");
639            return Ok(result);
640        }
641
642        debug!(
643            cached = result.len(),
644            uncached = uncached_uris.len(),
645            "cache lookup complete, querying database"
646        );
647
648        // Step 2: Register uncached URIs in batch
649        // Build query with validated table name (safe from SQL injection)
650        let query = format!(
651            r#"
652            INSERT INTO {} (uri)
653            SELECT unnest($1::text[])
654            ON CONFLICT (uri_hash) DO UPDATE SET uri = EXCLUDED.uri
655            RETURNING id, uri
656            "#,
657            self.table_name
658        );
659
660        // Execute with retry logic
661        let client = self.pool.get().await.map_err(|e| {
662            crate::error::Error::Database(format!("Failed to get database connection: {}", e))
663        })?;
664
665        let rows = client
666            .query(&query, &[&uncached_uris])
667            .await
668            .map_err(|e| crate::error::Error::Database(e.to_string()))?;
669
670        // Step 3: Add database results to result map and update cache
671        // CORRECTNESS: Each row contains both URI and ID from same DB row,
672        // guaranteeing correct mapping (no opportunity for misalignment)
673        for row in rows {
674            let uri: String = row.get("uri");
675            let id: i64 = row.get("id");
676            let id_u64 = id as u64;
677
678            result.insert(uri.clone(), id_u64); // URI and ID are from same row
679            self.cache.put(uri, id_u64);
680        }
681
682        Ok(result)
683    }
684}
685
686/// Statistics about the URI register for observability and OpenTelemetry
687#[derive(Debug, Clone)]
688pub struct RegisterStats {
689    /// Total number of URIs in the register
690    pub total_uris: u64,
691    /// Total storage size in bytes (includes indexes)
692    pub size_bytes: u64,
693    /// Cache performance metrics
694    pub cache: crate::cache::CacheStats,
695    /// Connection pool metrics
696    pub pool: PoolStats,
697}
698
699/// Connection pool statistics for observability
700#[derive(Debug, Clone)]
701pub struct PoolStats {
702    /// Number of connections currently being used
703    pub connections_active: u32,
704    /// Number of idle connections in the pool
705    pub connections_idle: u32,
706    /// Maximum number of connections allowed in the pool
707    pub connections_max: u32,
708}