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 (
164 crate::driver::rls::pool_release_commit_sql(),
165 "pool release reset/COMMIT",
166 )
167 } else {
168 (
169 crate::driver::rls::pool_release_rollback_sql(),
170 "pool release reset/ROLLBACK",
171 )
172 };
173 self.finish_with_reset(sql, context, "release_reset_failed")
174 .await
175 }
176
177 pub async fn rollback_and_release(self) -> PgResult<()> {
182 self.finish_with_reset(
183 crate::driver::rls::pool_release_rollback_sql(),
184 "pool release rollback/ROLLBACK",
185 "release_rollback_failed",
186 )
187 .await
188 }
189
190 pub async fn begin(&mut self) -> PgResult<()> {
208 self.reject_outer_transaction_control_in_rls("BEGIN")?;
209 self.conn_mut()?.begin_transaction().await
210 }
211
212 pub async fn commit(&mut self) -> PgResult<()> {
215 self.reject_outer_transaction_control_in_rls("COMMIT")?;
216 self.conn_mut()?.commit().await
217 }
218
219 pub async fn rollback(&mut self) -> PgResult<()> {
222 self.reject_outer_transaction_control_in_rls("ROLLBACK")?;
223 self.conn_mut()?.rollback().await
224 }
225
226 pub async fn savepoint(&mut self, name: &str) -> PgResult<()> {
229 self.conn_mut()?.savepoint(name).await
230 }
231
232 pub async fn rollback_to(&mut self, name: &str) -> PgResult<()> {
235 self.conn_mut()?.rollback_to(name).await
236 }
237
238 pub async fn release_savepoint(&mut self, name: &str) -> PgResult<()> {
241 self.conn_mut()?.release_savepoint(name).await
242 }
243
244 pub async fn pipeline_execute_rows_ast(
252 &mut self,
253 cmds: &[qail_core::ast::Qail],
254 ) -> PgResult<Vec<Vec<Vec<Option<Vec<u8>>>>>> {
255 let conn = self.conn_mut()?;
256 conn.pipeline_execute_rows_ast(cmds).await
257 }
258
259 pub async fn explain_estimate(
265 &mut self,
266 cmd: &qail_core::ast::Qail,
267 ) -> PgResult<Option<crate::driver::explain::ExplainEstimate>> {
268 let (sql, params) = crate::protocol::AstEncoder::encode_cmd_sql(cmd)
269 .map_err(|e| crate::driver::PgError::Encode(e.to_string()))?;
270 let explain_sql = format!("EXPLAIN (FORMAT JSON) {}", sql);
271
272 let rows = self.conn_mut()?.query(&explain_sql, ¶ms).await?;
273
274 let mut json_output = String::new();
276 for row in &rows {
277 if let Some(Some(val)) = row.first()
278 && let Ok(text) = std::str::from_utf8(val)
279 {
280 json_output.push_str(text);
281 }
282 }
283
284 Ok(crate::driver::explain::parse_explain_json(&json_output))
285 }
286
287 pub async fn listen(&mut self, channel: &str) -> PgResult<()> {
293 self.conn_mut()?.listen(channel).await
294 }
295
296 pub async fn unlisten(&mut self, channel: &str) -> PgResult<()> {
300 self.conn_mut()?.unlisten(channel).await
301 }
302
303 pub async fn unlisten_all(&mut self) -> PgResult<()> {
307 self.conn_mut()?.unlisten_all().await
308 }
309
310 pub async fn recv_notification(
315 &mut self,
316 ) -> PgResult<crate::driver::notification::Notification> {
317 self.conn_mut()?.recv_notification().await
318 }
319}
320
321impl Drop for PooledConnection {
322 fn drop(&mut self) {
323 if let Some(mut conn) = self.conn.take() {
324 tracing::warn!(
333 host = %self.pool.config.host,
334 port = self.pool.config.port,
335 user = %self.pool.config.user,
336 db = %self.pool.config.database,
337 rls_dirty = self.rls_dirty,
338 "pool_connection_leaked: dropped without release()"
339 );
340 if conn.is_io_desynced() {
341 tracing::warn!(
342 host = %self.pool.config.host,
343 port = self.pool.config.port,
344 user = %self.pool.config.user,
345 db = %self.pool.config.database,
346 "pool_connection_leaked_desynced: destroying immediately"
347 );
348 decrement_active_count_saturating(&self.pool.active_count);
349 self.pool.semaphore.add_permits(1);
350 pool_churn_record_destroy(&self.pool.config, "dropped_without_release_desynced");
351 return;
352 }
353
354 let mut inflight = self.pool.leaked_cleanup_inflight.load(Ordering::Relaxed);
355 let max_inflight = self.pool.config.leaked_cleanup_queue;
356 loop {
357 if inflight >= max_inflight {
358 tracing::warn!(
359 host = %self.pool.config.host,
360 port = self.pool.config.port,
361 user = %self.pool.config.user,
362 db = %self.pool.config.database,
363 max_inflight,
364 "pool_connection_leaked_cleanup_queue_full: destroying connection"
365 );
366 decrement_active_count_saturating(&self.pool.active_count);
367 self.pool.semaphore.add_permits(1);
368 pool_churn_record_destroy(
369 &self.pool.config,
370 "dropped_without_release_cleanup_queue_full",
371 );
372 return;
373 }
374
375 match self.pool.leaked_cleanup_inflight.compare_exchange_weak(
376 inflight,
377 inflight + 1,
378 Ordering::AcqRel,
379 Ordering::Relaxed,
380 ) {
381 Ok(_) => break,
382 Err(actual) => inflight = actual,
383 }
384 }
385
386 let pool = std::sync::Arc::clone(&self.pool);
387 let created_at = self.created_at;
388 let reset_timeout = pool.config.connect_timeout;
389 match tokio::runtime::Handle::try_current() {
390 Ok(handle) => {
391 handle.spawn(async move {
392 let cleanup_ok = execute_simple_with_timeout(
393 &mut conn,
394 crate::driver::rls::pool_release_rollback_sql(),
395 reset_timeout,
396 "pool leaked cleanup ROLLBACK",
397 )
398 .await
399 .is_ok();
400
401 if cleanup_ok && !conn.is_io_desynced() {
402 pool.return_connection(conn, created_at).await;
403 } else {
404 tracing::warn!(
405 host = %pool.config.host,
406 port = pool.config.port,
407 user = %pool.config.user,
408 db = %pool.config.database,
409 timeout_ms = reset_timeout.as_millis() as u64,
410 "pool_connection_leaked_cleanup_failed: destroying connection"
411 );
412 decrement_active_count_saturating(&pool.active_count);
413 pool.semaphore.add_permits(1);
414 pool_churn_record_destroy(
415 &pool.config,
416 "dropped_without_release_cleanup_failed",
417 );
418 }
419
420 pool.leaked_cleanup_inflight.fetch_sub(1, Ordering::AcqRel);
421 });
422 }
423 Err(_) => {
424 pool.leaked_cleanup_inflight.fetch_sub(1, Ordering::AcqRel);
425 tracing::warn!(
426 host = %pool.config.host,
427 port = pool.config.port,
428 user = %pool.config.user,
429 db = %pool.config.database,
430 "pool_connection_leaked_no_runtime: destroying connection"
431 );
432 decrement_active_count_saturating(&pool.active_count);
433 pool.semaphore.add_permits(1);
434 pool_churn_record_destroy(&pool.config, "dropped_without_release_no_runtime");
435 }
436 }
437 }
438 }
439}