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