Skip to main content

pleme_database/
pool.rs

1//! Database connection pool management
2//!
3//! Provides production-ready PostgreSQL connection pooling with:
4//! - Pool warming via min_connections (pre-established connections)
5//! - Idle timeout to release unused connections
6//! - Max lifetime to recycle connections (picks up PG config changes)
7//! - Connection health checks before checkout
8//!
9//! See `.claude/skills/connection-pooling-architecture` for full patterns.
10
11use crate::{DatabaseError, Result};
12use std::time::Duration;
13
14#[cfg(feature = "postgres")]
15use sqlx::{Pool, Postgres};
16
17/// Configuration for database connection pool
18#[derive(Clone, Debug)]
19pub struct PoolConfig {
20    /// Maximum number of connections in the pool (default: 20)
21    pub max_connections: u32,
22    /// Minimum connections to maintain (pool warming, default: 5)
23    pub min_connections: u32,
24    /// Time to wait for a connection from the pool (default: 30s)
25    pub acquire_timeout_secs: u64,
26    /// Release connections idle longer than this (default: 600s = 10 min)
27    pub idle_timeout_secs: u64,
28    /// Force recycle connections older than this (default: 1800s = 30 min)
29    pub max_lifetime_secs: u64,
30    /// Validate connection health before returning (default: true)
31    pub test_before_acquire: bool,
32}
33
34impl Default for PoolConfig {
35    fn default() -> Self {
36        Self {
37            max_connections: 20,
38            min_connections: 5,
39            acquire_timeout_secs: 30,
40            idle_timeout_secs: 600,
41            max_lifetime_secs: 1800,
42            test_before_acquire: true,
43        }
44    }
45}
46
47/// Database connection pool
48#[derive(Clone)]
49pub struct DatabasePool {
50    #[cfg(feature = "postgres")]
51    pool: Pool<Postgres>,
52}
53
54impl DatabasePool {
55    /// Connect to database with default configuration
56    #[cfg(feature = "postgres")]
57    pub async fn connect(url: &str) -> Result<Self> {
58        Self::connect_with_config(url, PoolConfig::default()).await
59    }
60
61    /// Connect to database with custom configuration
62    ///
63    /// Recommended settings by service type:
64    /// - High-concurrency (chat, search, order): max=25-30, min=10-15
65    /// - Medium-concurrency (auth, payment): max=20, min=5-10
66    /// - Low-concurrency (feature-flags, media): max=15, min=3-5
67    /// - One-shot binaries (extract-schema): max=1, min=0
68    #[cfg(feature = "postgres")]
69    pub async fn connect_with_config(url: &str, config: PoolConfig) -> Result<Self> {
70        let pool = sqlx::postgres::PgPoolOptions::new()
71            .max_connections(config.max_connections)
72            .min_connections(config.min_connections)
73            .acquire_timeout(Duration::from_secs(config.acquire_timeout_secs))
74            .idle_timeout(Some(Duration::from_secs(config.idle_timeout_secs)))
75            .max_lifetime(Some(Duration::from_secs(config.max_lifetime_secs)))
76            .test_before_acquire(config.test_before_acquire)
77            .connect(url)
78            .await
79            .map_err(|e| DatabaseError::ConnectionFailed(e.to_string()))?;
80
81        Ok(Self { pool })
82    }
83
84    /// Get underlying pool
85    #[cfg(feature = "postgres")]
86    pub fn inner(&self) -> &Pool<Postgres> {
87        &self.pool
88    }
89
90    /// Check database health
91    #[cfg(feature = "postgres")]
92    pub async fn health_check(&self) -> Result<()> {
93        sqlx::query("SELECT 1")
94            .execute(&self.pool)
95            .await
96            .map_err(|e| DatabaseError::QueryFailed(e.to_string()))?;
97        Ok(())
98    }
99}
100
101#[cfg(all(test, feature = "postgres"))]
102mod tests {
103    use super::*;
104
105    #[tokio::test]
106    #[ignore] // Requires database
107    async fn test_connect() {
108        let pool = DatabasePool::connect("postgres://localhost/test").await;
109        assert!(pool.is_ok() || pool.is_err()); // Either connection works or fails gracefully
110    }
111}