1use super::churn::{decrement_active_count_saturating, pool_churn_record_destroy};
5use super::lifecycle::{PgPoolInner, execute_simple_with_timeout};
6use crate::driver::{PgConnection, PgError, PgResult};
7use std::sync::Arc;
8use std::sync::atomic::Ordering;
9use std::time::Instant;
10
11pub(super) struct PooledConn {
13 pub(super) conn: PgConnection,
14 pub(super) created_at: Instant,
15 pub(super) last_used: Instant,
16}
17
18pub struct PooledConnection {
24 pub(super) conn: Option<PgConnection>,
25 pub(super) pool: Arc<PgPoolInner>,
26 pub(super) rls_dirty: bool,
27 pub(super) created_at: Instant,
28}
29
30impl PooledConnection {
31 pub(super) fn conn_ref(&self) -> PgResult<&PgConnection> {
34 self.conn
35 .as_ref()
36 .ok_or_else(|| PgError::Connection("Connection already released back to pool".into()))
37 }
38
39 pub(super) fn conn_mut(&mut self) -> PgResult<&mut PgConnection> {
42 self.conn
43 .as_mut()
44 .ok_or_else(|| PgError::Connection("Connection already released back to pool".into()))
45 }
46
47 pub fn get(&self) -> PgResult<&PgConnection> {
51 self.conn_ref()
52 }
53
54 pub fn get_mut(&mut self) -> PgResult<&mut PgConnection> {
58 self.conn_mut()
59 }
60
61 pub fn cancel_token(&self) -> PgResult<crate::driver::CancelToken> {
63 let conn = self.conn_ref()?;
64 let (process_id, secret_key_bytes) = conn.get_cancel_key_bytes();
65 Ok(crate::driver::CancelToken {
66 host: self.pool.config.host.clone(),
67 port: self.pool.config.port,
68 process_id,
69 secret_key_bytes: secret_key_bytes.to_vec(),
70 })
71 }
72
73 fn reject_outer_transaction_control_in_rls(&self, operation: &str) -> PgResult<()> {
74 if self.rls_dirty {
75 return Err(PgError::Connection(format!(
76 "{operation} is not allowed on an RLS-bound pooled connection; \
77 use savepoint(), rollback_to(), and release_savepoint() for nested work, \
78 then release() to close the pool-managed RLS transaction"
79 )));
80 }
81 Ok(())
82 }
83
84 async fn finish_with_reset(
85 mut self,
86 reset_sql: &'static str,
87 operation: &'static str,
88 failure_reason: &'static str,
89 ) -> PgResult<()> {
90 let Some(mut conn) = self.conn.take() else {
91 return Ok(());
92 };
93
94 if conn.is_io_desynced() {
95 tracing::warn!(
96 host = %self.pool.config.host,
97 port = self.pool.config.port,
98 user = %self.pool.config.user,
99 db = %self.pool.config.database,
100 "pool_release_desynced: dropping connection due to prior I/O/protocol desync"
101 );
102 decrement_active_count_saturating(&self.pool.active_count);
103 self.pool.semaphore.add_permits(1);
104 pool_churn_record_destroy(&self.pool.config, "release_desynced");
105 return Err(PgError::Connection(
106 "connection is protocol-desynced; dropped instead of returning to pool".into(),
107 ));
108 }
109
110 let reset_timeout = self.pool.config.connect_timeout;
111 if let Err(e) =
112 execute_simple_with_timeout(&mut conn, reset_sql, reset_timeout, operation).await
113 {
114 tracing::error!(
115 host = %self.pool.config.host,
116 port = self.pool.config.port,
117 user = %self.pool.config.user,
118 db = %self.pool.config.database,
119 timeout_ms = reset_timeout.as_millis() as u64,
120 error = %e,
121 "pool_release_failed: reset failed; dropping connection to prevent state leak"
122 );
123 decrement_active_count_saturating(&self.pool.active_count);
124 self.pool.semaphore.add_permits(1);
125 pool_churn_record_destroy(&self.pool.config, failure_reason);
126 return Err(e);
127 }
128
129 self.pool.return_connection(conn, self.created_at).await;
130 Ok(())
131 }
132
133 pub async fn release(self) {
150 let _ = self.release_checked().await;
151 }
152
153 pub async fn release_checked(self) -> PgResult<()> {
158 let (sql, context) = if self.rls_dirty {
159 (crate::driver::rls::reset_sql(), "pool release reset/COMMIT")
163 } else {
164 ("ROLLBACK", "pool release reset/ROLLBACK")
165 };
166 self.finish_with_reset(sql, context, "release_reset_failed")
167 .await
168 }
169
170 pub async fn rollback_and_release(self) -> PgResult<()> {
175 self.finish_with_reset(
176 "ROLLBACK",
177 "pool release rollback/ROLLBACK",
178 "release_rollback_failed",
179 )
180 .await
181 }
182
183 pub async fn begin(&mut self) -> PgResult<()> {
201 self.reject_outer_transaction_control_in_rls("BEGIN")?;
202 self.conn_mut()?.begin_transaction().await
203 }
204
205 pub async fn commit(&mut self) -> PgResult<()> {
208 self.reject_outer_transaction_control_in_rls("COMMIT")?;
209 self.conn_mut()?.commit().await
210 }
211
212 pub async fn rollback(&mut self) -> PgResult<()> {
215 self.reject_outer_transaction_control_in_rls("ROLLBACK")?;
216 self.conn_mut()?.rollback().await
217 }
218
219 pub async fn savepoint(&mut self, name: &str) -> PgResult<()> {
222 self.conn_mut()?.savepoint(name).await
223 }
224
225 pub async fn rollback_to(&mut self, name: &str) -> PgResult<()> {
228 self.conn_mut()?.rollback_to(name).await
229 }
230
231 pub async fn release_savepoint(&mut self, name: &str) -> PgResult<()> {
234 self.conn_mut()?.release_savepoint(name).await
235 }
236
237 pub async fn pipeline_execute_rows_ast(
245 &mut self,
246 cmds: &[qail_core::ast::Qail],
247 ) -> PgResult<Vec<Vec<Vec<Option<Vec<u8>>>>>> {
248 let conn = self.conn_mut()?;
249 conn.pipeline_execute_rows_ast(cmds).await
250 }
251
252 pub async fn explain_estimate(
258 &mut self,
259 cmd: &qail_core::ast::Qail,
260 ) -> PgResult<Option<crate::driver::explain::ExplainEstimate>> {
261 let (sql, params) = crate::protocol::AstEncoder::encode_cmd_sql(cmd)
262 .map_err(|e| crate::driver::PgError::Encode(e.to_string()))?;
263 let explain_sql = format!("EXPLAIN (FORMAT JSON) {}", sql);
264
265 let rows = self.conn_mut()?.query(&explain_sql, ¶ms).await?;
266
267 let mut json_output = String::new();
269 for row in &rows {
270 if let Some(Some(val)) = row.first()
271 && let Ok(text) = std::str::from_utf8(val)
272 {
273 json_output.push_str(text);
274 }
275 }
276
277 Ok(crate::driver::explain::parse_explain_json(&json_output))
278 }
279
280 pub async fn listen(&mut self, channel: &str) -> PgResult<()> {
286 self.conn_mut()?.listen(channel).await
287 }
288
289 pub async fn unlisten(&mut self, channel: &str) -> PgResult<()> {
293 self.conn_mut()?.unlisten(channel).await
294 }
295
296 pub async fn unlisten_all(&mut self) -> PgResult<()> {
300 self.conn_mut()?.unlisten_all().await
301 }
302
303 pub async fn recv_notification(
308 &mut self,
309 ) -> PgResult<crate::driver::notification::Notification> {
310 self.conn_mut()?.recv_notification().await
311 }
312}
313
314impl Drop for PooledConnection {
315 fn drop(&mut self) {
316 if let Some(mut conn) = self.conn.take() {
317 tracing::warn!(
326 host = %self.pool.config.host,
327 port = self.pool.config.port,
328 user = %self.pool.config.user,
329 db = %self.pool.config.database,
330 rls_dirty = self.rls_dirty,
331 "pool_connection_leaked: dropped without release()"
332 );
333 if conn.is_io_desynced() {
334 tracing::warn!(
335 host = %self.pool.config.host,
336 port = self.pool.config.port,
337 user = %self.pool.config.user,
338 db = %self.pool.config.database,
339 "pool_connection_leaked_desynced: destroying immediately"
340 );
341 decrement_active_count_saturating(&self.pool.active_count);
342 self.pool.semaphore.add_permits(1);
343 pool_churn_record_destroy(&self.pool.config, "dropped_without_release_desynced");
344 return;
345 }
346
347 let mut inflight = self.pool.leaked_cleanup_inflight.load(Ordering::Relaxed);
348 let max_inflight = self.pool.config.leaked_cleanup_queue;
349 loop {
350 if inflight >= max_inflight {
351 tracing::warn!(
352 host = %self.pool.config.host,
353 port = self.pool.config.port,
354 user = %self.pool.config.user,
355 db = %self.pool.config.database,
356 max_inflight,
357 "pool_connection_leaked_cleanup_queue_full: destroying connection"
358 );
359 decrement_active_count_saturating(&self.pool.active_count);
360 self.pool.semaphore.add_permits(1);
361 pool_churn_record_destroy(
362 &self.pool.config,
363 "dropped_without_release_cleanup_queue_full",
364 );
365 return;
366 }
367
368 match self.pool.leaked_cleanup_inflight.compare_exchange_weak(
369 inflight,
370 inflight + 1,
371 Ordering::AcqRel,
372 Ordering::Relaxed,
373 ) {
374 Ok(_) => break,
375 Err(actual) => inflight = actual,
376 }
377 }
378
379 let pool = std::sync::Arc::clone(&self.pool);
380 let created_at = self.created_at;
381 let reset_timeout = pool.config.connect_timeout;
382 match tokio::runtime::Handle::try_current() {
383 Ok(handle) => {
384 handle.spawn(async move {
385 let cleanup_ok = execute_simple_with_timeout(
386 &mut conn,
387 "ROLLBACK",
388 reset_timeout,
389 "pool leaked cleanup ROLLBACK",
390 )
391 .await
392 .is_ok();
393
394 if cleanup_ok && !conn.is_io_desynced() {
395 pool.return_connection(conn, created_at).await;
396 } else {
397 tracing::warn!(
398 host = %pool.config.host,
399 port = pool.config.port,
400 user = %pool.config.user,
401 db = %pool.config.database,
402 timeout_ms = reset_timeout.as_millis() as u64,
403 "pool_connection_leaked_cleanup_failed: destroying connection"
404 );
405 decrement_active_count_saturating(&pool.active_count);
406 pool.semaphore.add_permits(1);
407 pool_churn_record_destroy(
408 &pool.config,
409 "dropped_without_release_cleanup_failed",
410 );
411 }
412
413 pool.leaked_cleanup_inflight.fetch_sub(1, Ordering::AcqRel);
414 });
415 }
416 Err(_) => {
417 pool.leaked_cleanup_inflight.fetch_sub(1, Ordering::AcqRel);
418 tracing::warn!(
419 host = %pool.config.host,
420 port = pool.config.port,
421 user = %pool.config.user,
422 db = %pool.config.database,
423 "pool_connection_leaked_no_runtime: destroying connection"
424 );
425 decrement_active_count_saturating(&pool.active_count);
426 pool.semaphore.add_permits(1);
427 pool_churn_record_destroy(&pool.config, "dropped_without_release_no_runtime");
428 }
429 }
430 }
431 }
432}