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(pg_config, crate::tls::make_tls_connector(), mgr_config),
48 };
49 #[cfg(not(feature = "tls"))]
50 let mgr = match config.ssl_mode {
51 SslMode::Disable => Manager::from_config(pg_config, NoTls, mgr_config),
52 SslMode::Prefer => Manager::from_config(pg_config, NoTls, mgr_config),
58 other => {
59 return Err(PgError::config(format!(
60 "ssl_mode {:?} requires TLS support; rebuild prax-postgres \
61 with the `tls` feature (enabled by default)",
62 other
63 )));
64 }
65 };
66
67 let mut builder = Pool::builder(mgr).max_size(pool_config.max_connections);
69
70 if let Some(timeout) = pool_config.connection_timeout {
72 builder = builder
73 .wait_timeout(Some(timeout))
74 .create_timeout(Some(timeout));
75 }
76 if let Some(timeout) = pool_config.idle_timeout {
77 builder = builder.recycle_timeout(Some(timeout));
78 }
79
80 builder = builder.runtime(Runtime::Tokio1);
82
83 let pool = builder
84 .build()
85 .map_err(|e| PgError::config(format!("failed to create pool: {}", e)))?;
86
87 let min_connections = pool_config.min_connections.min(pool_config.max_connections);
93 if min_connections > 0 {
94 let warm_pool = pool.clone();
95 tokio::spawn(async move {
96 let mut held = Vec::with_capacity(min_connections);
97 for _ in 0..min_connections {
98 match warm_pool.get().await {
99 Ok(conn) => {
100 debug!(
101 established = held.len() + 1,
102 min_connections = min_connections,
103 "min_connections warmup established connection"
104 );
105 held.push(conn);
106 }
107 Err(e) => {
108 warn!(
111 error = %e,
112 min_connections = min_connections,
113 established = held.len(),
114 "min_connections warmup could not acquire connection; \
115 pool starts below its configured minimum"
116 );
117 break;
118 }
119 }
120 }
121 drop(held);
122 });
123 }
124
125 info!(
126 host = %config.host,
127 port = %config.port,
128 database = %config.database,
129 max_connections = %pool_config.max_connections,
130 "PostgreSQL connection pool created"
131 );
132
133 Ok(Self {
134 inner: pool,
135 config: Arc::new(config),
136 statement_cache: Arc::new(PreparedStatementCache::new(
137 pool_config.statement_cache_size,
138 )),
139 })
140 }
141
142 pub async fn get(&self) -> PgResult<PgConnection> {
144 debug!("Acquiring connection from pool");
145 let client = self.inner.get().await?;
146 Ok(PgConnection::new(client, self.statement_cache.clone()))
147 }
148
149 pub(crate) fn inner(&self) -> &Pool {
157 &self.inner
158 }
159
160 pub fn status(&self) -> PoolStatus {
162 let status = self.inner.status();
163 PoolStatus {
164 available: status.available,
165 size: status.size,
166 max_size: status.max_size,
167 waiting: status.waiting,
168 }
169 }
170
171 pub fn config(&self) -> &PgConfig {
173 &self.config
174 }
175
176 pub async fn is_healthy(&self) -> bool {
178 match self.inner.get().await {
179 Ok(client) => {
180 client.query_one("SELECT 1", &[]).await.is_ok()
182 }
183 Err(_) => false,
184 }
185 }
186
187 pub fn close(&self) {
189 self.inner.close();
190 info!("PostgreSQL connection pool closed");
191 }
192
193 pub fn builder() -> PgPoolBuilder {
195 PgPoolBuilder::new()
196 }
197
198 pub async fn warmup(&self, count: usize) -> PgResult<()> {
216 info!(count = count, "Warming up connection pool");
217
218 let count = count.min(self.inner.status().max_size);
219 let mut connections = Vec::with_capacity(count);
220
221 for i in 0..count {
223 match self.inner.get().await {
224 Ok(conn) => {
225 if let Err(e) = conn.query_one("SELECT 1", &[]).await {
227 debug!(error = %e, "Warmup connection {} failed validation", i);
228 } else {
229 debug!("Warmup connection {} established", i);
230 connections.push(conn);
231 }
232 }
233 Err(e) => {
234 debug!(error = %e, "Failed to establish warmup connection {}", i);
235 }
236 }
237 }
238
239 let established = connections.len();
241 drop(connections);
242
243 info!(
244 established = established,
245 requested = count,
246 "Connection pool warmup complete"
247 );
248
249 Ok(())
250 }
251
252 pub async fn warmup_with_statements(&self, count: usize, statements: &[&str]) -> PgResult<()> {
257 info!(
258 count = count,
259 statements = statements.len(),
260 "Warming up connection pool with prepared statements"
261 );
262
263 let count = count.min(self.inner.status().max_size);
264 let mut connections = Vec::with_capacity(count);
265
266 for i in 0..count {
267 match self.inner.get().await {
268 Ok(conn) => {
269 for sql in statements {
271 if let Err(e) = conn.prepare_cached(sql).await {
272 debug!(error = %e, sql = %sql, "Failed to prepare statement");
273 }
274 }
275 debug!(
276 connection = i,
277 statements = statements.len(),
278 "Prepared statements on connection"
279 );
280 connections.push(conn);
281 }
282 Err(e) => {
283 debug!(error = %e, "Failed to establish warmup connection {}", i);
284 }
285 }
286 }
287
288 let established = connections.len();
289 drop(connections);
290
291 info!(
292 established = established,
293 "Connection pool warmup with statements complete"
294 );
295
296 Ok(())
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct PoolStatus {
303 pub available: usize,
305 pub size: usize,
307 pub max_size: usize,
309 pub waiting: usize,
311}
312
313#[derive(Debug, Clone)]
315pub struct PoolConfig {
316 pub max_connections: usize,
318 pub min_connections: usize,
324 pub connection_timeout: Option<Duration>,
326 pub idle_timeout: Option<Duration>,
333 pub max_lifetime: Option<Duration>,
338 pub statement_cache_size: usize,
344}
345
346impl Default for PoolConfig {
347 fn default() -> Self {
348 Self {
349 max_connections: 10,
350 min_connections: 1,
351 connection_timeout: Some(Duration::from_secs(30)),
352 idle_timeout: Some(Duration::from_secs(600)), max_lifetime: Some(Duration::from_secs(1800)), statement_cache_size: 100,
355 }
356 }
357}
358
359#[derive(Debug, Default)]
361pub struct PgPoolBuilder {
362 config: Option<PgConfig>,
363 url: Option<String>,
364 pool_config: PoolConfig,
365}
366
367impl PgPoolBuilder {
368 pub fn new() -> Self {
370 Self {
371 config: None,
372 url: None,
373 pool_config: PoolConfig::default(),
374 }
375 }
376
377 pub fn url(mut self, url: impl Into<String>) -> Self {
379 self.url = Some(url.into());
380 self
381 }
382
383 pub fn config(mut self, config: PgConfig) -> Self {
385 self.config = Some(config);
386 self
387 }
388
389 pub fn max_connections(mut self, n: usize) -> Self {
391 self.pool_config.max_connections = n;
392 self
393 }
394
395 pub fn min_connections(mut self, n: usize) -> Self {
397 self.pool_config.min_connections = n;
398 self
399 }
400
401 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
403 self.pool_config.connection_timeout = Some(timeout);
404 self
405 }
406
407 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
409 self.pool_config.idle_timeout = Some(timeout);
410 self
411 }
412
413 pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
415 self.pool_config.max_lifetime = Some(lifetime);
416 self
417 }
418
419 pub fn statement_cache_size(mut self, size: usize) -> Self {
421 self.pool_config.statement_cache_size = size;
422 self
423 }
424
425 pub async fn build(self) -> PgResult<PgPool> {
427 let config = if let Some(config) = self.config {
428 config
429 } else if let Some(url) = self.url {
430 PgConfig::from_url(url)?
431 } else {
432 return Err(PgError::config("no database URL or config provided"));
433 };
434
435 PgPool::with_pool_config(config, self.pool_config).await
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_pool_config_default() {
445 let config = PoolConfig::default();
446 assert_eq!(config.max_connections, 10);
447 assert_eq!(config.min_connections, 1);
448 assert_eq!(config.statement_cache_size, 100);
449 }
450
451 #[test]
452 fn test_pool_builder() {
453 let builder = PgPoolBuilder::new()
454 .url("postgresql://localhost/test")
455 .max_connections(20)
456 .statement_cache_size(200);
457
458 assert!(builder.url.is_some());
459 assert_eq!(builder.pool_config.max_connections, 20);
460 assert_eq!(builder.pool_config.statement_cache_size, 200);
461 }
462}