1use std::sync::Arc;
4use std::time::Duration;
5
6use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod, Runtime};
7use tokio_postgres::NoTls;
8use tracing::{debug, info, warn};
9
10use crate::config::{PgConfig, SslMode};
11use crate::connection::PgConnection;
12use crate::error::{PgError, PgResult};
13use crate::statement::PreparedStatementCache;
14
15#[derive(Clone)]
17pub struct PgPool {
18 inner: Pool,
19 config: Arc<PgConfig>,
20 statement_cache: Arc<PreparedStatementCache>,
21}
22
23impl PgPool {
24 pub async fn new(config: PgConfig) -> PgResult<Self> {
26 Self::with_pool_config(config, PoolConfig::default()).await
27 }
28
29 pub async fn with_pool_config(config: PgConfig, pool_config: PoolConfig) -> PgResult<Self> {
31 let pg_config = config.to_pg_config();
32
33 let mgr_config = ManagerConfig {
34 recycling_method: RecyclingMethod::Fast,
35 };
36
37 #[cfg(feature = "tls")]
43 let mgr = match config.ssl_mode {
44 SslMode::Disable => Manager::from_config(pg_config, NoTls, mgr_config),
45 _ => Manager::from_config(
53 pg_config,
54 crate::tls::make_tls_connector_with_root_cert(config.ssl_root_cert.as_deref())?,
55 mgr_config,
56 ),
57 };
58 #[cfg(not(feature = "tls"))]
59 let mgr = match config.ssl_mode {
60 SslMode::Disable => Manager::from_config(pg_config, NoTls, mgr_config),
61 SslMode::Prefer => Manager::from_config(pg_config, NoTls, mgr_config),
67 other => {
68 return Err(PgError::config(format!(
69 "ssl_mode {:?} requires TLS support; rebuild prax-postgres \
70 with the `tls` feature (enabled by default)",
71 other
72 )));
73 }
74 };
75
76 let mut builder = Pool::builder(mgr).max_size(pool_config.max_connections);
78
79 if let Some(timeout) = pool_config.connection_timeout {
81 builder = builder
82 .wait_timeout(Some(timeout))
83 .create_timeout(Some(timeout));
84 }
85 if let Some(timeout) = pool_config.idle_timeout {
86 builder = builder.recycle_timeout(Some(timeout));
87 }
88
89 builder = builder.runtime(Runtime::Tokio1);
91
92 let pool = builder
93 .build()
94 .map_err(|e| PgError::config(format!("failed to create pool: {}", e)))?;
95
96 let min_connections = pool_config.min_connections.min(pool_config.max_connections);
102 if min_connections > 0 {
103 let warm_pool = pool.clone();
104 tokio::spawn(async move {
105 let mut held = Vec::with_capacity(min_connections);
106 for _ in 0..min_connections {
107 match warm_pool.get().await {
108 Ok(conn) => {
109 debug!(
110 established = held.len() + 1,
111 min_connections = min_connections,
112 "min_connections warmup established connection"
113 );
114 held.push(conn);
115 }
116 Err(e) => {
117 warn!(
120 error = %e,
121 min_connections = min_connections,
122 established = held.len(),
123 "min_connections warmup could not acquire connection; \
124 pool starts below its configured minimum"
125 );
126 break;
127 }
128 }
129 }
130 drop(held);
131 });
132 }
133
134 info!(
135 host = %config.host,
136 port = %config.port,
137 database = %config.database,
138 max_connections = %pool_config.max_connections,
139 "PostgreSQL connection pool created"
140 );
141
142 Ok(Self {
143 inner: pool,
144 config: Arc::new(config),
145 statement_cache: Arc::new(PreparedStatementCache::new(
146 pool_config.statement_cache_size,
147 )),
148 })
149 }
150
151 pub async fn get(&self) -> PgResult<PgConnection> {
153 debug!("Acquiring connection from pool");
154 let client = self.inner.get().await?;
155 Ok(PgConnection::new(client, self.statement_cache.clone()))
156 }
157
158 pub(crate) fn inner(&self) -> &Pool {
166 &self.inner
167 }
168
169 pub fn status(&self) -> PoolStatus {
171 let status = self.inner.status();
172 PoolStatus {
173 available: status.available,
174 size: status.size,
175 max_size: status.max_size,
176 waiting: status.waiting,
177 }
178 }
179
180 pub fn config(&self) -> &PgConfig {
182 &self.config
183 }
184
185 pub async fn is_healthy(&self) -> bool {
187 match self.inner.get().await {
188 Ok(client) => {
189 client.query_one("SELECT 1", &[]).await.is_ok()
191 }
192 Err(_) => false,
193 }
194 }
195
196 pub fn close(&self) {
198 self.inner.close();
199 info!("PostgreSQL connection pool closed");
200 }
201
202 pub fn builder() -> PgPoolBuilder {
204 PgPoolBuilder::new()
205 }
206
207 pub async fn warmup(&self, count: usize) -> PgResult<()> {
225 info!(count = count, "Warming up connection pool");
226
227 let count = count.min(self.inner.status().max_size);
228 let mut connections = Vec::with_capacity(count);
229
230 for i in 0..count {
232 match self.inner.get().await {
233 Ok(conn) => {
234 if let Err(e) = conn.query_one("SELECT 1", &[]).await {
236 debug!(error = %e, "Warmup connection {} failed validation", i);
237 } else {
238 debug!("Warmup connection {} established", i);
239 connections.push(conn);
240 }
241 }
242 Err(e) => {
243 debug!(error = %e, "Failed to establish warmup connection {}", i);
244 }
245 }
246 }
247
248 let established = connections.len();
250 drop(connections);
251
252 info!(
253 established = established,
254 requested = count,
255 "Connection pool warmup complete"
256 );
257
258 Ok(())
259 }
260
261 pub async fn warmup_with_statements(&self, count: usize, statements: &[&str]) -> PgResult<()> {
266 info!(
267 count = count,
268 statements = statements.len(),
269 "Warming up connection pool with prepared statements"
270 );
271
272 let count = count.min(self.inner.status().max_size);
273 let mut connections = Vec::with_capacity(count);
274
275 for i in 0..count {
276 match self.inner.get().await {
277 Ok(conn) => {
278 for sql in statements {
280 if let Err(e) = conn.prepare_cached(sql).await {
281 debug!(error = %e, sql = %sql, "Failed to prepare statement");
282 }
283 }
284 debug!(
285 connection = i,
286 statements = statements.len(),
287 "Prepared statements on connection"
288 );
289 connections.push(conn);
290 }
291 Err(e) => {
292 debug!(error = %e, "Failed to establish warmup connection {}", i);
293 }
294 }
295 }
296
297 let established = connections.len();
298 drop(connections);
299
300 info!(
301 established = established,
302 "Connection pool warmup with statements complete"
303 );
304
305 Ok(())
306 }
307}
308
309#[derive(Debug, Clone)]
311pub struct PoolStatus {
312 pub available: usize,
314 pub size: usize,
316 pub max_size: usize,
318 pub waiting: usize,
320}
321
322#[derive(Debug, Clone)]
324pub struct PoolConfig {
325 pub max_connections: usize,
327 pub min_connections: usize,
333 pub connection_timeout: Option<Duration>,
335 pub idle_timeout: Option<Duration>,
342 pub max_lifetime: Option<Duration>,
347 pub statement_cache_size: usize,
353}
354
355impl Default for PoolConfig {
356 fn default() -> Self {
357 Self {
358 max_connections: 10,
359 min_connections: 1,
360 connection_timeout: Some(Duration::from_secs(30)),
361 idle_timeout: Some(Duration::from_secs(600)), max_lifetime: Some(Duration::from_secs(1800)), statement_cache_size: 100,
364 }
365 }
366}
367
368#[derive(Debug, Default)]
370pub struct PgPoolBuilder {
371 config: Option<PgConfig>,
372 url: Option<String>,
373 pool_config: PoolConfig,
374}
375
376impl PgPoolBuilder {
377 pub fn new() -> Self {
379 Self {
380 config: None,
381 url: None,
382 pool_config: PoolConfig::default(),
383 }
384 }
385
386 pub fn url(mut self, url: impl Into<String>) -> Self {
388 self.url = Some(url.into());
389 self
390 }
391
392 pub fn config(mut self, config: PgConfig) -> Self {
394 self.config = Some(config);
395 self
396 }
397
398 pub fn max_connections(mut self, n: usize) -> Self {
400 self.pool_config.max_connections = n;
401 self
402 }
403
404 pub fn min_connections(mut self, n: usize) -> Self {
406 self.pool_config.min_connections = n;
407 self
408 }
409
410 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
412 self.pool_config.connection_timeout = Some(timeout);
413 self
414 }
415
416 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
418 self.pool_config.idle_timeout = Some(timeout);
419 self
420 }
421
422 pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
424 self.pool_config.max_lifetime = Some(lifetime);
425 self
426 }
427
428 pub fn statement_cache_size(mut self, size: usize) -> Self {
430 self.pool_config.statement_cache_size = size;
431 self
432 }
433
434 pub async fn build(self) -> PgResult<PgPool> {
436 let config = if let Some(config) = self.config {
437 config
438 } else if let Some(url) = self.url {
439 PgConfig::from_url(url)?
440 } else {
441 return Err(PgError::config("no database URL or config provided"));
442 };
443
444 PgPool::with_pool_config(config, self.pool_config).await
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_pool_config_default() {
454 let config = PoolConfig::default();
455 assert_eq!(config.max_connections, 10);
456 assert_eq!(config.min_connections, 1);
457 assert_eq!(config.statement_cache_size, 100);
458 }
459
460 #[test]
461 fn test_pool_builder() {
462 let builder = PgPoolBuilder::new()
463 .url("postgresql://localhost/test")
464 .max_connections(20)
465 .statement_cache_size(200);
466
467 assert!(builder.url.is_some());
468 assert_eq!(builder.pool_config.max_connections, 20);
469 assert_eq!(builder.pool_config.statement_cache_size, 200);
470 }
471}