1use super::ScopedPoolFuture;
5use super::churn::{
6 PoolStats, decrement_active_count_saturating, pool_churn_record_destroy,
7 pool_churn_remaining_open, record_pool_connection_destroy,
8};
9use super::config::PoolConfig;
10use super::connection::PooledConn;
11use super::connection::PooledConnection;
12use super::gss::*;
13use crate::driver::{
14 AstPipelineMode, AutoCountPath, AutoCountPlan, ConnectOptions, PgConnection, PgError, PgResult,
15 is_ignorable_session_message, unexpected_backend_message,
16};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
19use std::time::{Duration, Instant};
20use tokio::sync::{Mutex, Semaphore};
21use tokio::task::JoinSet;
22
23pub(super) const MAX_HOT_STATEMENTS: usize = 32;
25
26pub(super) struct PgPoolInner {
28 pub(super) config: PoolConfig,
29 pub(super) connections: Mutex<Vec<PooledConn>>,
30 pub(super) semaphore: Semaphore,
31 pub(super) closed: AtomicBool,
32 pub(super) active_count: AtomicUsize,
33 pub(super) total_created: AtomicUsize,
34 pub(super) leaked_cleanup_inflight: AtomicUsize,
35 pub(super) hot_statements: std::sync::RwLock<std::collections::HashMap<u64, (String, String)>>,
39}
40
41pub(super) fn handle_hot_preprepare_message(
42 msg: &crate::protocol::BackendMessage,
43 parse_complete_count: &mut usize,
44 error: &mut Option<PgError>,
45) -> PgResult<bool> {
46 match msg {
47 crate::protocol::BackendMessage::ParseComplete => {
48 *parse_complete_count += 1;
49 Ok(false)
50 }
51 crate::protocol::BackendMessage::ErrorResponse(err) => {
52 if error.is_none() {
53 *error = Some(PgError::QueryServer(err.clone().into()));
54 }
55 Ok(false)
56 }
57 crate::protocol::BackendMessage::ReadyForQuery(_) => Ok(true),
58 msg if is_ignorable_session_message(msg) => Ok(false),
59 other => Err(unexpected_backend_message("pool hot pre-prepare", other)),
60 }
61}
62
63fn evict_failed_hot_preprepare_entries(
64 pool: &PgPoolInner,
65 missing: &[(u64, String, String)],
66) -> usize {
67 let Ok(mut hot) = pool.hot_statements.write() else {
68 return 0;
69 };
70
71 let mut evicted = 0usize;
72 for (hash, _, _) in missing {
73 if hot.remove(hash).is_some() {
74 evicted += 1;
75 }
76 }
77 evicted
78}
79
80impl PgPoolInner {
81 pub(super) async fn return_connection(&self, mut conn: PgConnection, created_at: Instant) {
82 decrement_active_count_saturating(&self.active_count);
83
84 conn.notifications.clear();
89
90 if conn.is_io_desynced() {
91 tracing::warn!(
92 host = %self.config.host,
93 port = self.config.port,
94 user = %self.config.user,
95 db = %self.config.database,
96 "pool_return_desynced: dropping connection due to prior I/O/protocol desync"
97 );
98 record_pool_connection_destroy("pool_desynced_drop");
99 self.semaphore.add_permits(1);
100 pool_churn_record_destroy(&self.config, "return_desynced");
101 return;
102 }
103
104 if self.closed.load(Ordering::Relaxed) {
105 record_pool_connection_destroy("pool_closed_drop");
106 self.semaphore.add_permits(1);
107 return;
108 }
109
110 let mut connections = self.connections.lock().await;
111 if connections.len() < self.config.max_connections {
112 connections.push(PooledConn {
113 conn,
114 created_at,
115 last_used: Instant::now(),
116 });
117 } else {
118 record_pool_connection_destroy("pool_overflow_drop");
119 }
120
121 self.semaphore.add_permits(1);
122 }
123
124 async fn get_healthy_connection(&self) -> Option<PooledConn> {
126 let mut connections = self.connections.lock().await;
127
128 while let Some(pooled) = connections.pop() {
129 if pooled.last_used.elapsed() > self.config.idle_timeout {
130 tracing::debug!(
131 idle_secs = pooled.last_used.elapsed().as_secs(),
132 timeout_secs = self.config.idle_timeout.as_secs(),
133 "pool_checkout_evict: connection exceeded idle timeout"
134 );
135 record_pool_connection_destroy("idle_timeout_evict");
136 continue;
137 }
138
139 if let Some(max_life) = self.config.max_lifetime
140 && pooled.created_at.elapsed() > max_life
141 {
142 tracing::debug!(
143 age_secs = pooled.created_at.elapsed().as_secs(),
144 max_lifetime_secs = max_life.as_secs(),
145 "pool_checkout_evict: connection exceeded max lifetime"
146 );
147 record_pool_connection_destroy("max_lifetime_evict");
148 continue;
149 }
150
151 return Some(pooled);
152 }
153
154 None
155 }
156}
157
158#[derive(Clone)]
169pub struct PgPool {
170 pub(super) inner: Arc<PgPoolInner>,
171}
172
173impl PgPool {
174 pub async fn from_config() -> PgResult<Self> {
181 let qail = qail_core::config::QailConfig::load()
182 .map_err(|e| PgError::Connection(format!("Config error: {}", e)))?;
183 let config = PoolConfig::from_qail_config(&qail)?;
184 Self::connect(config).await
185 }
186
187 pub async fn connect(config: PoolConfig) -> PgResult<Self> {
189 validate_pool_config(&config)?;
190
191 let semaphore = Semaphore::new(config.max_connections);
193
194 let mut initial_connections = Vec::new();
195 for _ in 0..config.min_connections {
196 let conn = Self::create_connection(&config).await?;
197 initial_connections.push(PooledConn {
198 conn,
199 created_at: Instant::now(),
200 last_used: Instant::now(),
201 });
202 }
203
204 let initial_count = initial_connections.len();
205
206 let inner = Arc::new(PgPoolInner {
207 config,
208 connections: Mutex::new(initial_connections),
209 semaphore,
210 closed: AtomicBool::new(false),
211 active_count: AtomicUsize::new(0),
212 total_created: AtomicUsize::new(initial_count),
213 leaked_cleanup_inflight: AtomicUsize::new(0),
214 hot_statements: std::sync::RwLock::new(std::collections::HashMap::new()),
215 });
216
217 Ok(Self { inner })
218 }
219
220 pub async fn acquire_raw(&self) -> PgResult<PooledConnection> {
235 if self.inner.closed.load(Ordering::Relaxed) {
236 return Err(PgError::PoolClosed);
237 }
238
239 if let Some(remaining) = pool_churn_remaining_open(&self.inner.config) {
240 metrics::counter!("qail_pg_pool_churn_circuit_reject_total").increment(1);
241 tracing::warn!(
242 host = %self.inner.config.host,
243 port = self.inner.config.port,
244 user = %self.inner.config.user,
245 db = %self.inner.config.database,
246 remaining_ms = remaining.as_millis() as u64,
247 "pool_connection_churn_circuit_open"
248 );
249 return Err(PgError::PoolExhausted {
250 max: self.inner.config.max_connections,
251 });
252 }
253
254 let acquire_timeout = self.inner.config.acquire_timeout;
256 let permit =
257 match tokio::time::timeout(acquire_timeout, self.inner.semaphore.acquire()).await {
258 Ok(permit) => permit.map_err(|_| PgError::PoolClosed)?,
259 Err(_) => {
260 metrics::counter!("qail_pg_pool_acquire_timeouts_total").increment(1);
261 return Err(PgError::Timeout(format!(
262 "pool acquire after {}s ({} max connections)",
263 acquire_timeout.as_secs(),
264 self.inner.config.max_connections
265 )));
266 }
267 };
268
269 if self.inner.closed.load(Ordering::Relaxed) {
270 return Err(PgError::PoolClosed);
271 }
272
273 let (mut conn, mut created_at) =
275 if let Some(pooled) = self.inner.get_healthy_connection().await {
276 (pooled.conn, pooled.created_at)
277 } else {
278 let conn = Self::create_connection(&self.inner.config).await?;
279 self.inner.total_created.fetch_add(1, Ordering::Relaxed);
280 (conn, Instant::now())
281 };
282
283 if self.inner.config.test_on_acquire
284 && let Err(e) = execute_simple_with_timeout(
285 &mut conn,
286 "SELECT 1",
287 self.inner.config.connect_timeout,
288 "pool checkout health check",
289 )
290 .await
291 {
292 tracing::warn!(
293 host = %self.inner.config.host,
294 port = self.inner.config.port,
295 user = %self.inner.config.user,
296 db = %self.inner.config.database,
297 error = %e,
298 "pool_health_check_failed: checkout probe failed, creating replacement connection"
299 );
300 pool_churn_record_destroy(&self.inner.config, "health_check_failed");
301 conn = Self::create_connection(&self.inner.config).await?;
302 self.inner.total_created.fetch_add(1, Ordering::Relaxed);
303 created_at = Instant::now();
304 }
305
306 let missing: Vec<(u64, String, String)> = {
309 if let Ok(hot) = self.inner.hot_statements.read() {
310 hot.iter()
311 .filter(|(hash, _)| !conn.stmt_cache.contains(hash))
312 .map(|(hash, (name, sql))| (*hash, name.clone(), sql.clone()))
313 .collect()
314 } else {
315 Vec::new()
316 }
317 }; if !missing.is_empty() {
320 use crate::protocol::PgEncoder;
321 let mut buf = bytes::BytesMut::new();
322 for (_, name, sql) in &missing {
323 let parse_msg = PgEncoder::try_encode_parse(name, sql, &[])?;
324 buf.extend_from_slice(&parse_msg);
325 }
326 PgEncoder::encode_sync_to(&mut buf);
327 let preprepare_timeout = self.inner.config.connect_timeout;
328 let preprepare_result: PgResult<()> = match tokio::time::timeout(
329 preprepare_timeout,
330 async {
331 conn.send_bytes(&buf).await?;
332 let mut parse_complete_count = 0usize;
334 let mut parse_error: Option<PgError> = None;
335 loop {
336 let msg = conn.recv().await?;
337 if handle_hot_preprepare_message(
338 &msg,
339 &mut parse_complete_count,
340 &mut parse_error,
341 )? {
342 if let Some(err) = parse_error {
343 return Err(err);
344 }
345 if parse_complete_count != missing.len() {
346 return Err(PgError::Protocol(format!(
347 "hot pre-prepare completed with {} ParseComplete messages (expected {})",
348 parse_complete_count,
349 missing.len()
350 )));
351 }
352 break;
353 }
354 }
355 Ok::<(), PgError>(())
356 },
357 )
358 .await
359 {
360 Ok(res) => res,
361 Err(_) => Err(PgError::Timeout(format!(
362 "hot statement pre-prepare timeout after {:?} (pool config connect_timeout)",
363 preprepare_timeout
364 ))),
365 };
366
367 if let Err(e) = preprepare_result {
368 let evicted_hot_statements =
369 evict_failed_hot_preprepare_entries(&self.inner, &missing);
370 tracing::warn!(
371 host = %self.inner.config.host,
372 port = self.inner.config.port,
373 user = %self.inner.config.user,
374 db = %self.inner.config.database,
375 timeout_ms = preprepare_timeout.as_millis() as u64,
376 evicted_hot_statements,
377 error = %e,
378 "pool_hot_prepare_failed: replacing connection to avoid handing out uncertain protocol state"
379 );
380 pool_churn_record_destroy(&self.inner.config, "hot_prepare_failed");
381 conn = Self::create_connection(&self.inner.config).await?;
382 self.inner.total_created.fetch_add(1, Ordering::Relaxed);
383 created_at = Instant::now();
384 } else {
385 for (hash, name, sql) in &missing {
387 conn.stmt_cache.put(*hash, name.clone());
388 conn.prepared_statements.insert(name.clone(), sql.clone());
389 }
390 }
391 }
392
393 self.inner.active_count.fetch_add(1, Ordering::Relaxed);
394 permit.forget();
396
397 Ok(PooledConnection {
398 conn: Some(conn),
399 pool: std::sync::Arc::clone(&self.inner),
400 rls_dirty: false,
401 created_at,
402 })
403 }
404
405 pub async fn acquire_with_rls(
421 &self,
422 ctx: qail_core::rls::RlsContext,
423 ) -> PgResult<PooledConnection> {
424 let mut conn = self.acquire_raw().await?;
426
427 let sql = crate::driver::rls::context_to_sql(&ctx);
429 let pg_conn = conn.get_mut()?;
430 if let Err(e) = execute_simple_with_timeout(
431 pg_conn,
432 &sql,
433 self.inner.config.connect_timeout,
434 "pool acquire_with_rls setup",
435 )
436 .await
437 {
438 if let Ok(pg_conn) = conn.get_mut() {
441 let _ = pg_conn.execute_simple("ROLLBACK").await;
442 }
443 conn.release().await;
444 return Err(e);
445 }
446
447 conn.rls_dirty = true;
449
450 Ok(conn)
451 }
452
453 pub async fn with_rls<T, F>(&self, ctx: qail_core::rls::RlsContext, f: F) -> PgResult<T>
457 where
458 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
459 {
460 let mut conn = self.acquire_with_rls(ctx).await?;
461 let out = f(&mut conn).await;
462 match out {
463 Ok(value) => {
464 conn.release_checked().await?;
465 Ok(value)
466 }
467 Err(err) => {
468 let _ = conn.rollback_and_release().await;
469 Err(err)
470 }
471 }
472 }
473
474 pub async fn with_system<T, F>(&self, f: F) -> PgResult<T>
476 where
477 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
478 {
479 self.with_rls(qail_core::rls::RlsContext::empty(), f).await
480 }
481
482 pub async fn with_global<T, F>(&self, f: F) -> PgResult<T>
484 where
485 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
486 {
487 self.with_rls(qail_core::rls::RlsContext::global(), f).await
488 }
489
490 pub async fn with_tenant<T, F>(&self, tenant_id: &str, f: F) -> PgResult<T>
492 where
493 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
494 {
495 self.with_rls(qail_core::rls::RlsContext::tenant(tenant_id), f)
496 .await
497 }
498
499 pub async fn acquire_with_rls_timeout(
504 &self,
505 ctx: qail_core::rls::RlsContext,
506 timeout_ms: u32,
507 ) -> PgResult<PooledConnection> {
508 let mut conn = self.acquire_raw().await?;
510
511 let sql = crate::driver::rls::context_to_sql_with_timeout(&ctx, timeout_ms);
513 let pg_conn = conn.get_mut()?;
514 if let Err(e) = execute_simple_with_timeout(
515 pg_conn,
516 &sql,
517 self.inner.config.connect_timeout,
518 "pool acquire_with_rls_timeout setup",
519 )
520 .await
521 {
522 if let Ok(pg_conn) = conn.get_mut() {
523 let _ = pg_conn.execute_simple("ROLLBACK").await;
524 }
525 conn.release().await;
526 return Err(e);
527 }
528
529 conn.rls_dirty = true;
531
532 Ok(conn)
533 }
534
535 pub async fn with_rls_timeout<T, F>(
537 &self,
538 ctx: qail_core::rls::RlsContext,
539 timeout_ms: u32,
540 f: F,
541 ) -> PgResult<T>
542 where
543 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
544 {
545 let mut conn = self.acquire_with_rls_timeout(ctx, timeout_ms).await?;
546 let out = f(&mut conn).await;
547 match out {
548 Ok(value) => {
549 conn.release_checked().await?;
550 Ok(value)
551 }
552 Err(err) => {
553 let _ = conn.rollback_and_release().await;
554 Err(err)
555 }
556 }
557 }
558
559 pub async fn acquire_with_rls_timeouts(
565 &self,
566 ctx: qail_core::rls::RlsContext,
567 statement_timeout_ms: u32,
568 lock_timeout_ms: u32,
569 ) -> PgResult<PooledConnection> {
570 let mut conn = self.acquire_raw().await?;
572
573 let sql = crate::driver::rls::context_to_sql_with_timeouts(
574 &ctx,
575 statement_timeout_ms,
576 lock_timeout_ms,
577 );
578 let pg_conn = conn.get_mut()?;
579 if let Err(e) = execute_simple_with_timeout(
580 pg_conn,
581 &sql,
582 self.inner.config.connect_timeout,
583 "pool acquire_with_rls_timeouts setup",
584 )
585 .await
586 {
587 if let Ok(pg_conn) = conn.get_mut() {
588 let _ = pg_conn.execute_simple("ROLLBACK").await;
589 }
590 conn.release().await;
591 return Err(e);
592 }
593
594 conn.rls_dirty = true;
595
596 Ok(conn)
597 }
598
599 pub async fn with_rls_timeouts<T, F>(
601 &self,
602 ctx: qail_core::rls::RlsContext,
603 statement_timeout_ms: u32,
604 lock_timeout_ms: u32,
605 f: F,
606 ) -> PgResult<T>
607 where
608 F: for<'a> FnOnce(&'a mut PooledConnection) -> ScopedPoolFuture<'a, T>,
609 {
610 let mut conn = self
611 .acquire_with_rls_timeouts(ctx, statement_timeout_ms, lock_timeout_ms)
612 .await?;
613 let out = f(&mut conn).await;
614 match out {
615 Ok(value) => {
616 conn.release_checked().await?;
617 Ok(value)
618 }
619 Err(err) => {
620 let _ = conn.rollback_and_release().await;
621 Err(err)
622 }
623 }
624 }
625
626 pub async fn acquire_system(&self) -> PgResult<PooledConnection> {
635 let ctx = qail_core::rls::RlsContext::empty();
636 self.acquire_with_rls(ctx).await
637 }
638
639 pub async fn acquire_global(&self) -> PgResult<PooledConnection> {
645 self.acquire_with_rls(qail_core::rls::RlsContext::global())
646 .await
647 }
648
649 pub async fn acquire_for_tenant(&self, tenant_id: &str) -> PgResult<PooledConnection> {
661 self.acquire_with_rls(qail_core::rls::RlsContext::tenant(tenant_id))
662 .await
663 }
664
665 pub async fn acquire_with_branch(
679 &self,
680 ctx: &qail_core::branch::BranchContext,
681 ) -> PgResult<PooledConnection> {
682 let mut conn = self.acquire_raw().await?;
684
685 if let Some(branch_name) = ctx.branch_name() {
686 let sql = crate::driver::branch_sql::branch_context_sql(branch_name);
687 let pg_conn = conn.get_mut()?;
688 if let Err(e) = execute_simple_with_timeout(
689 pg_conn,
690 &sql,
691 self.inner.config.connect_timeout,
692 "pool acquire_with_branch setup",
693 )
694 .await
695 {
696 if let Ok(pg_conn) = conn.get_mut() {
697 let _ = pg_conn.execute_simple("ROLLBACK").await;
698 }
699 conn.release().await;
700 return Err(e);
701 }
702 conn.rls_dirty = true; }
704
705 Ok(conn)
706 }
707
708 pub async fn idle_count(&self) -> usize {
710 self.inner.connections.lock().await.len()
711 }
712
713 pub fn active_count(&self) -> usize {
715 self.inner.active_count.load(Ordering::Relaxed)
716 }
717
718 pub fn max_connections(&self) -> usize {
720 self.inner.config.max_connections
721 }
722
723 pub fn plan_auto_count(&self, batch_len: usize) -> AutoCountPlan {
725 AutoCountPlan::for_pool(
726 batch_len,
727 self.inner.config.max_connections,
728 self.inner.semaphore.available_permits(),
729 )
730 }
731
732 pub async fn execute_count_auto_with_plan(
734 &self,
735 cmds: &[qail_core::ast::Qail],
736 ) -> PgResult<(usize, AutoCountPlan)> {
737 let plan = self.plan_auto_count(cmds.len());
738
739 let completed = match plan.path {
740 AutoCountPath::SingleCached => {
741 if cmds.is_empty() {
742 0
743 } else {
744 let mut conn = self.acquire_system().await?;
745 let run_result = conn.fetch_all_cached(&cmds[0]).await;
746 conn.release().await;
747 let _ = run_result?;
748 1
749 }
750 }
751 AutoCountPath::PipelineOneShot | AutoCountPath::PipelineCached => {
752 let mode = if matches!(plan.path, AutoCountPath::PipelineOneShot) {
753 AstPipelineMode::OneShot
754 } else {
755 AstPipelineMode::Cached
756 };
757
758 let mut pooled = self.acquire_system().await?;
759 let run_result = {
760 let conn = pooled.get_mut()?;
761 conn.pipeline_execute_count_ast_with_mode(cmds, mode).await
762 };
763 pooled.release().await;
764 run_result?
765 }
766 AutoCountPath::PoolParallel => {
767 if cmds.is_empty() {
768 0
769 } else {
770 let all_cmds = Arc::new(cmds.to_vec());
771 let mut tasks: JoinSet<PgResult<usize>> = JoinSet::new();
772
773 for worker in 0..plan.workers {
774 let start = worker * plan.chunk_size;
775 if start >= all_cmds.len() {
776 break;
777 }
778 let end = (start + plan.chunk_size).min(all_cmds.len());
779 let pool = self.clone();
780 let all_cmds = Arc::clone(&all_cmds);
781
782 tasks.spawn(async move {
783 let mut pooled = pool.acquire_system().await?;
784 let run_result = {
785 let conn = pooled.get_mut()?;
786 conn.pipeline_execute_count_ast_with_mode(
787 &all_cmds[start..end],
788 AstPipelineMode::Auto,
789 )
790 .await
791 };
792 pooled.release().await;
793 run_result
794 });
795 }
796
797 let mut total = 0usize;
798 while let Some(joined) = tasks.join_next().await {
799 match joined {
800 Ok(Ok(count)) => {
801 total += count;
802 }
803 Ok(Err(err)) => return Err(err),
804 Err(err) => {
805 return Err(PgError::Connection(format!(
806 "auto pool worker join failed: {err}"
807 )));
808 }
809 }
810 }
811 total
812 }
813 }
814 };
815
816 Ok((completed, plan))
817 }
818
819 #[inline]
821 pub async fn execute_count_auto(&self, cmds: &[qail_core::ast::Qail]) -> PgResult<usize> {
822 let (completed, _plan) = self.execute_count_auto_with_plan(cmds).await?;
823 Ok(completed)
824 }
825
826 pub async fn stats(&self) -> PoolStats {
828 let idle = self.inner.connections.lock().await.len();
829 let active = self.inner.active_count.load(Ordering::Relaxed);
830 let used_slots = self
831 .inner
832 .config
833 .max_connections
834 .saturating_sub(self.inner.semaphore.available_permits());
835 PoolStats {
836 active,
837 idle,
838 pending: used_slots.saturating_sub(active),
839 max_size: self.inner.config.max_connections,
840 total_created: self.inner.total_created.load(Ordering::Relaxed),
841 }
842 }
843
844 pub fn is_closed(&self) -> bool {
846 self.inner.closed.load(Ordering::Relaxed)
847 }
848
849 pub async fn close(&self) {
856 self.close_graceful(self.inner.config.acquire_timeout).await;
857 }
858
859 pub async fn close_graceful(&self, drain_timeout: Duration) {
861 self.inner.closed.store(true, Ordering::Relaxed);
862 self.inner.semaphore.close();
864
865 let deadline = Instant::now() + drain_timeout;
866 loop {
867 let active = self.inner.active_count.load(Ordering::Relaxed);
868 if active == 0 {
869 break;
870 }
871 if Instant::now() >= deadline {
872 tracing::warn!(
873 active_connections = active,
874 timeout_ms = drain_timeout.as_millis() as u64,
875 "pool_close_drain_timeout: forcing idle cleanup while active connections remain"
876 );
877 break;
878 }
879 tokio::time::sleep(Duration::from_millis(25)).await;
880 }
881
882 let mut connections = self.inner.connections.lock().await;
883 let dropped_idle = connections.len();
884 connections.clear();
885 tracing::info!(
886 dropped_idle_connections = dropped_idle,
887 active_connections = self.inner.active_count.load(Ordering::Relaxed),
888 "pool_closed"
889 );
890 }
891
892 async fn create_connection(config: &PoolConfig) -> PgResult<PgConnection> {
894 if !config.auth_settings.has_any_password_method()
895 && config.mtls.is_none()
896 && config.password.is_some()
897 {
898 return Err(PgError::Auth(
899 "Invalid PoolConfig: all password auth methods are disabled".to_string(),
900 ));
901 }
902
903 let options = ConnectOptions {
904 tls_mode: config.tls_mode,
905 gss_enc_mode: config.gss_enc_mode,
906 tls_ca_cert_pem: config.tls_ca_cert_pem.clone(),
907 mtls: config.mtls.clone(),
908 gss_token_provider: config.gss_token_provider.clone(),
909 auth: config.auth_settings,
910 io_uring: config.io_uring,
911 startup_params: Vec::new(),
912 };
913
914 if let Some(remaining) = gss_circuit_remaining_open(config) {
915 metrics::counter!("qail_pg_gss_circuit_open_total").increment(1);
916 tracing::warn!(
917 host = %config.host,
918 port = config.port,
919 user = %config.user,
920 db = %config.database,
921 remaining_ms = remaining.as_millis() as u64,
922 "gss_connect_circuit_open"
923 );
924 return Err(PgError::Connection(format!(
925 "GSS connection circuit is open; retry after {:?}",
926 remaining
927 )));
928 }
929
930 let mut attempt = 0usize;
931 loop {
932 let connect_result = tokio::time::timeout(
933 config.connect_timeout,
934 PgConnection::connect_with_options(
935 &config.host,
936 config.port,
937 &config.user,
938 &config.database,
939 config.password.as_deref(),
940 options.clone(),
941 ),
942 )
943 .await;
944
945 let connect_result = match connect_result {
946 Ok(result) => result,
947 Err(_) => Err(PgError::Timeout(format!(
948 "connect timeout after {:?} (pool config connect_timeout)",
949 config.connect_timeout
950 ))),
951 };
952
953 match connect_result {
954 Ok(conn) => {
955 metrics::counter!("qail_pg_pool_connect_success_total").increment(1);
956 gss_circuit_record_success(config);
957 return Ok(conn);
958 }
959 Err(err) if should_retry_gss_connect_error(config, attempt, &err) => {
960 metrics::counter!("qail_pg_gss_connect_retries_total").increment(1);
961 gss_circuit_record_failure(config);
962 let delay = gss_retry_delay(config.gss_retry_base_delay, attempt);
963 tracing::warn!(
964 host = %config.host,
965 port = config.port,
966 user = %config.user,
967 db = %config.database,
968 attempt = attempt + 1,
969 delay_ms = delay.as_millis() as u64,
970 error = %err,
971 "gss_connect_retry"
972 );
973 tokio::time::sleep(delay).await;
974 attempt += 1;
975 }
976 Err(err) => {
977 metrics::counter!("qail_pg_pool_connect_failures_total").increment(1);
978 if should_track_gss_circuit_error(config, &err) {
979 metrics::counter!("qail_pg_gss_connect_failures_total").increment(1);
980 gss_circuit_record_failure(config);
981 }
982 return Err(err);
983 }
984 }
985 }
986 }
987
988 pub async fn maintain(&self) {
991 if self.inner.closed.load(Ordering::Relaxed) {
992 return;
993 }
994
995 let evicted = {
997 let mut connections = self.inner.connections.lock().await;
998 let before = connections.len();
999 connections.retain(|pooled| {
1000 if pooled.last_used.elapsed() > self.inner.config.idle_timeout {
1001 record_pool_connection_destroy("idle_sweep_evict");
1002 return false;
1003 }
1004 if let Some(max_life) = self.inner.config.max_lifetime
1005 && pooled.created_at.elapsed() > max_life
1006 {
1007 record_pool_connection_destroy("lifetime_sweep_evict");
1008 return false;
1009 }
1010 true
1011 });
1012 before - connections.len()
1013 };
1014
1015 if evicted > 0 {
1016 tracing::debug!(evicted, "pool_maintenance: evicted stale idle connections");
1017 }
1018
1019 let min = self.inner.config.min_connections;
1021 if min == 0 {
1022 return;
1023 }
1024
1025 let idle_count = self.inner.connections.lock().await.len();
1026 let checked_out_slots = self
1027 .inner
1028 .config
1029 .max_connections
1030 .saturating_sub(self.inner.semaphore.available_permits());
1031 let deficit = maintenance_backfill_deficit(
1032 self.inner.config.max_connections,
1033 min,
1034 idle_count,
1035 checked_out_slots,
1036 );
1037 if deficit == 0 {
1038 return;
1039 }
1040 let mut created = 0usize;
1041 for _ in 0..deficit {
1042 match Self::create_connection(&self.inner.config).await {
1043 Ok(conn) => {
1044 self.inner.total_created.fetch_add(1, Ordering::Relaxed);
1045 let mut connections = self.inner.connections.lock().await;
1046 if connections.len() < self.inner.config.max_connections {
1047 connections.push(PooledConn {
1048 conn,
1049 created_at: Instant::now(),
1050 last_used: Instant::now(),
1051 });
1052 created += 1;
1053 } else {
1054 break;
1056 }
1057 }
1058 Err(e) => {
1059 tracing::warn!(error = %e, "pool_maintenance: backfill connection failed");
1060 break; }
1062 }
1063 }
1064
1065 if created > 0 {
1066 tracing::debug!(
1067 created,
1068 min_connections = min,
1069 "pool_maintenance: backfilled idle connections"
1070 );
1071 }
1072 }
1073}
1074
1075pub fn spawn_pool_maintenance(pool: PgPool) {
1080 let interval_secs = std::cmp::max(pool.inner.config.idle_timeout.as_secs() / 2, 5);
1081 tokio::spawn(async move {
1082 let mut interval = tokio::time::interval(Duration::from_secs(interval_secs));
1083 loop {
1084 interval.tick().await;
1085 if pool.is_closed() {
1086 break;
1087 }
1088 pool.maintain().await;
1089 }
1090 });
1091}
1092
1093pub(super) fn maintenance_backfill_deficit(
1094 max_connections: usize,
1095 min_connections: usize,
1096 idle_count: usize,
1097 checked_out_slots: usize,
1098) -> usize {
1099 let target_idle = min_connections.min(max_connections);
1100 if idle_count >= target_idle {
1101 return 0;
1102 }
1103
1104 let needed_idle = target_idle - idle_count;
1105 let available_slots =
1106 max_connections.saturating_sub(idle_count.saturating_add(checked_out_slots));
1107 needed_idle.min(available_slots)
1108}
1109
1110pub(super) fn validate_pool_config(config: &PoolConfig) -> PgResult<()> {
1111 if config.max_connections == 0 {
1112 return Err(PgError::Connection(
1113 "Invalid PoolConfig: max_connections must be >= 1".to_string(),
1114 ));
1115 }
1116 if config.min_connections > config.max_connections {
1117 return Err(PgError::Connection(format!(
1118 "Invalid PoolConfig: min_connections ({}) must be <= max_connections ({})",
1119 config.min_connections, config.max_connections
1120 )));
1121 }
1122 if config.acquire_timeout.is_zero() {
1123 return Err(PgError::Connection(
1124 "Invalid PoolConfig: acquire_timeout must be > 0".to_string(),
1125 ));
1126 }
1127 if config.connect_timeout.is_zero() {
1128 return Err(PgError::Connection(
1129 "Invalid PoolConfig: connect_timeout must be > 0".to_string(),
1130 ));
1131 }
1132 if config.leaked_cleanup_queue == 0 {
1133 return Err(PgError::Connection(
1134 "Invalid PoolConfig: leaked_cleanup_queue must be >= 1".to_string(),
1135 ));
1136 }
1137 Ok(())
1138}
1139
1140pub(super) async fn execute_simple_with_timeout(
1141 conn: &mut PgConnection,
1142 sql: &str,
1143 timeout: Duration,
1144 operation: &str,
1145) -> PgResult<()> {
1146 match tokio::time::timeout(timeout, conn.execute_simple(sql)).await {
1147 Ok(result) => result,
1148 Err(_) => {
1149 conn.mark_io_desynced();
1150 Err(PgError::Timeout(format!(
1151 "{} timeout after {:?} (pool config connect_timeout)",
1152 operation, timeout
1153 )))
1154 }
1155 }
1156}