Skip to main content

elefant_client/
pool.rs

1use std::ops::{Deref, DerefMut};
2use std::sync::{Arc, Mutex};
3
4use crate::protocol::async_io::ElefantAsyncReadWrite;
5use crate::protocol::PostgresConnection;
6use crate::types::EnumTypeRegistry;
7use crate::{ElefantClientError, PostgresConnectionSettings};
8use tracing::debug;
9
10pub trait ConnectionFactory {
11    type Connection: ElefantAsyncReadWrite;
12
13    fn create_connection(
14        &self,
15        settings: &PostgresConnectionSettings,
16    ) -> impl std::future::Future<Output = Result<Self::Connection, ElefantClientError>>;
17}
18
19pub struct PostgresPool<F: ConnectionFactory>(Arc<PostgresPoolInner<F>>);
20
21struct PostgresPoolInner<F: ConnectionFactory> {
22    factory: F,
23    settings: PostgresConnectionSettings,
24    idle_connections: Mutex<Vec<crate::postgres_client::PostgresClient<F>>>,
25    enum_registry: Arc<EnumTypeRegistry>,
26}
27
28impl<F: ConnectionFactory> Clone for PostgresPool<F> {
29    fn clone(&self) -> Self {
30        PostgresPool(Arc::clone(&self.0))
31    }
32}
33
34impl<F: ConnectionFactory> PostgresPool<F> {
35    pub async fn new(
36        factory: F,
37        settings: PostgresConnectionSettings,
38    ) -> Result<Self, ElefantClientError> {
39        let empty_registry = Arc::new(EnumTypeRegistry::new());
40
41        if settings.enum_type_names().is_empty() {
42            return Ok(PostgresPool(Arc::new(PostgresPoolInner {
43                factory,
44                settings,
45                idle_connections: Mutex::new(Vec::new()),
46                enum_registry: empty_registry,
47            })));
48        }
49
50        // Create a temporary connection to query enum OIDs
51        let raw_stream = factory.create_connection(&settings).await?;
52        let (connection, channel_binding_data) =
53            PostgresConnection::new_maybe_tls(raw_stream, &settings).await?;
54        let mut client = crate::postgres_client::PostgresClient::new(
55            connection,
56            &settings,
57            empty_registry,
58            channel_binding_data,
59        )
60        .await?;
61
62        // Build the SQL query filtering by the registered enum names
63        let names_sql = settings
64            .enum_type_names()
65            .iter()
66            .map(|n| format!("'{}'", n.replace('\'', "''")))
67            .collect::<Vec<_>>()
68            .join(", ");
69
70        let query = format!(
71            "SELECT t.typname, n.nspname, t.oid::int4, t.typarray::int4 \
72             FROM pg_catalog.pg_type t \
73             JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid \
74             WHERE t.typtype = 'e' AND (t.typname IN ({names_sql}) \
75                   OR (n.nspname || '.' || t.typname) IN ({names_sql}))"
76        );
77
78        let mut registry = EnumTypeRegistry::new();
79
80        let mut query_result = client.query_simple(&query).await?;
81        loop {
82            let result_set = query_result.next_result_set().await?;
83            match result_set {
84                crate::postgres_client::QueryResultSet::QueryProcessingComplete => break,
85                crate::postgres_client::QueryResultSet::RowDescriptionReceived(mut row_reader) => {
86                    while let Some(row) = row_reader.next_row().await? {
87                        let typname: &str = row.get_text(0)?;
88                        let nspname: &str = row.get_text(1)?;
89                        let oid: i32 = row.get_text(2)?;
90                        let typarray: i32 = row.get_text(3)?;
91
92                        let qualified_name = format!("{nspname}.{typname}");
93
94                        // Insert under the unqualified name
95                        registry.insert(
96                            typname.to_string(),
97                            crate::types::EnumOidEntry {
98                                oid,
99                                array_oid: typarray,
100                                schema: nspname.to_string(),
101                            },
102                        );
103
104                        // Also insert under the schema-qualified name
105                        registry.insert(
106                            qualified_name,
107                            crate::types::EnumOidEntry {
108                                oid,
109                                array_oid: typarray,
110                                schema: nspname.to_string(),
111                            },
112                        );
113                    }
114                }
115            }
116        }
117
118        let registry = Arc::new(registry);
119        client.enum_registry = registry.clone();
120
121        // Return the client to the idle pool for reuse
122        Ok(PostgresPool(Arc::new(PostgresPoolInner {
123            factory,
124            settings,
125            idle_connections: Mutex::new(vec![client]),
126            enum_registry: registry,
127        })))
128    }
129
130    pub fn settings(&self) -> &PostgresConnectionSettings {
131        &self.0.settings
132    }
133
134    pub fn enum_registry(&self) -> &Arc<EnumTypeRegistry> {
135        &self.0.enum_registry
136    }
137
138    pub(crate) fn return_connection(&self, mut client: crate::postgres_client::PostgresClient<F>) {
139        client.pool = None;
140        self.0.idle_connections.lock().unwrap().push(client);
141    }
142
143    /// Gracefully close all idle connections by sending Terminate to each.
144    /// Currently checked-out clients are not affected — close them individually
145    /// via [`PoolableClient::close`] or let them return to the pool first.
146    pub async fn close(&self) -> Result<(), ElefantClientError> {
147        let clients: Vec<_> = self.0.idle_connections.lock().unwrap().drain(..).collect();
148        for client in clients {
149            client.close().await?;
150        }
151        Ok(())
152    }
153
154    pub async fn get_client(&self) -> Result<PoolableClient<F>, ElefantClientError> {
155        // Try to reuse an idle connection
156        loop {
157            let idle_client = { self.0.idle_connections.lock().unwrap().pop() };
158            match idle_client {
159                Some(mut client) => match client.reset().await {
160                    Ok(()) => {
161                        client.pool = Some(self.clone());
162                        client.enum_registry = self.0.enum_registry.clone();
163                        return Ok(PoolableClient {
164                            client: Some(client),
165                            pool: self.clone(),
166                        });
167                    }
168                    Err(e) => {
169                        debug!("Idle connection reset failed, discarding: {:?}", e);
170                        continue;
171                    }
172                },
173                None => break,
174            }
175        }
176
177        // No idle connection available, create a new one
178        let raw_stream = self.0.factory.create_connection(&self.0.settings).await?;
179        let (connection, channel_binding_data) =
180            PostgresConnection::new_maybe_tls(raw_stream, &self.0.settings).await?;
181        let mut client = crate::postgres_client::PostgresClient::new(
182            connection,
183            &self.0.settings,
184            self.0.enum_registry.clone(),
185            channel_binding_data,
186        )
187        .await?;
188        client.pool = Some(self.clone());
189        Ok(PoolableClient {
190            client: Some(client),
191            pool: self.clone(),
192        })
193    }
194}
195
196pub struct PoolableClient<F: ConnectionFactory> {
197    client: Option<crate::postgres_client::PostgresClient<F>>,
198    pool: PostgresPool<F>,
199}
200
201impl<F: ConnectionFactory> Deref for PoolableClient<F> {
202    type Target = crate::postgres_client::PostgresClient<F>;
203
204    fn deref(&self) -> &Self::Target {
205        self.client
206            .as_ref()
207            .expect("client already returned to pool")
208    }
209}
210
211impl<F: ConnectionFactory> DerefMut for PoolableClient<F> {
212    fn deref_mut(&mut self) -> &mut Self::Target {
213        self.client
214            .as_mut()
215            .expect("client already returned to pool")
216    }
217}
218
219impl<F: ConnectionFactory> PoolableClient<F> {
220    /// Gracefully close this connection by sending Terminate to the backend.
221    /// The connection is not returned to the pool. Consumes the client.
222    pub async fn close(mut self) -> Result<(), ElefantClientError> {
223        match self.client.take() {
224            Some(client) => client.close().await,
225            None => Ok(()),
226        }
227    }
228}
229
230impl<F: ConnectionFactory> Drop for PoolableClient<F> {
231    fn drop(&mut self) {
232        if let Some(client) = self.client.take() {
233            self.pool.return_connection(client);
234        }
235    }
236}
237
238#[cfg(test)]
239impl<F: ConnectionFactory> PostgresPool<F> {
240    pub(crate) fn idle_connection_count(&self) -> usize {
241        self.0.idle_connections.lock().unwrap().len()
242    }
243}
244
245#[cfg(all(test, feature = "tokio"))]
246mod tests {
247    use crate::test_helpers::get_settings;
248    use crate::tokio_connection::{new_client, TokioConnectionFactory, TokioPostgresPool};
249
250    #[tokio::test]
251    async fn pool_reuses_connection_after_drop() {
252        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
253            .await
254            .unwrap();
255
256        let pid1: i32;
257        {
258            let mut client = pool.get_client().await.unwrap();
259            pid1 = client
260                .read_single_value_simple("select pg_backend_pid()")
261                .await;
262        }
263
264        let mut client2 = pool.get_client().await.unwrap();
265        let pid2: i32 = client2
266            .read_single_value_simple("select pg_backend_pid()")
267            .await;
268
269        assert_eq!(pid1, pid2, "Pool should reuse the same connection");
270    }
271
272    #[tokio::test]
273    async fn pool_creates_new_connection_when_all_checked_out() {
274        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
275            .await
276            .unwrap();
277
278        let mut client1 = pool.get_client().await.unwrap();
279        let mut client2 = pool.get_client().await.unwrap();
280
281        let pid1: i32 = client1
282            .read_single_value_simple("select pg_backend_pid()")
283            .await;
284        let pid2: i32 = client2
285            .read_single_value_simple("select pg_backend_pid()")
286            .await;
287
288        assert_ne!(
289            pid1, pid2,
290            "Simultaneously checked-out clients must be different connections"
291        );
292    }
293
294    #[tokio::test]
295    async fn pool_idle_count_grows_on_drop() {
296        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
297            .await
298            .unwrap();
299
300        assert_eq!(pool.idle_connection_count(), 0);
301
302        let client1 = pool.get_client().await.unwrap();
303        let client2 = pool.get_client().await.unwrap();
304        assert_eq!(pool.idle_connection_count(), 0);
305
306        drop(client1);
307        assert_eq!(pool.idle_connection_count(), 1);
308
309        drop(client2);
310        assert_eq!(pool.idle_connection_count(), 2);
311    }
312
313    #[tokio::test]
314    async fn pool_reset_rolls_back_uncommitted_transaction() {
315        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
316            .await
317            .unwrap();
318
319        {
320            let mut client = pool.get_client().await.unwrap();
321            client.execute_non_query_simple("BEGIN").await.unwrap();
322            client
323                .execute_non_query_simple("CREATE TEMP TABLE pool_txn_test (id int)")
324                .await
325                .unwrap();
326            // Intentionally do NOT commit -- drop returns to pool
327        }
328
329        // get_client() calls reset() which rolls back the transaction
330        let mut client = pool.get_client().await.unwrap();
331
332        // The temp table should not exist because the transaction was rolled back
333        let result = client
334            .execute_non_query_simple("SELECT 1 FROM pool_txn_test")
335            .await;
336        assert!(
337            result.is_err(),
338            "Temp table should not exist after rollback"
339        );
340    }
341
342    #[tokio::test]
343    async fn pool_reset_recovers_from_failed_transaction() {
344        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
345            .await
346            .unwrap();
347
348        {
349            let mut client = pool.get_client().await.unwrap();
350            client.execute_non_query_simple("BEGIN").await.unwrap();
351            // This will fail and put the connection into InFailedTransaction state
352            let _ = client
353                .execute_non_query_simple("SELECT * FROM nonexistent_table_that_does_not_exist")
354                .await;
355            // Drop without ROLLBACK or COMMIT
356        }
357
358        // reset() should detect InFailedTransaction and issue ROLLBACK
359        let mut client = pool.get_client().await.unwrap();
360        let value: i32 = client.read_single_value_simple("select 42").await;
361        assert_eq!(value, 42);
362    }
363
364    #[tokio::test]
365    async fn pool_clone_shares_state() {
366        let pool1 = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
367            .await
368            .unwrap();
369        let pool2 = pool1.clone();
370
371        let pid1: i32;
372        {
373            let mut client = pool1.get_client().await.unwrap();
374            pid1 = client
375                .read_single_value_simple("select pg_backend_pid()")
376                .await;
377        }
378
379        let mut client2 = pool2.get_client().await.unwrap();
380        let pid2: i32 = client2
381            .read_single_value_simple("select pg_backend_pid()")
382            .await;
383
384        assert_eq!(
385            pid1, pid2,
386            "Cloned pool should share the idle connection vec"
387        );
388    }
389
390    #[tokio::test]
391    async fn pool_client_works_across_multiple_reuse_cycles() {
392        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
393            .await
394            .unwrap();
395
396        for i in 0..5 {
397            let mut client = pool.get_client().await.unwrap();
398            let value: i32 = client
399                .read_single_value_simple(&format!("select {}", i + 1))
400                .await;
401            assert_eq!(value, i + 1);
402        }
403    }
404
405    #[tokio::test]
406    async fn pool_close_terminates_connections() {
407        let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
408            .await
409            .unwrap();
410
411        let mut client1 = pool.get_client().await.unwrap();
412        let mut client2 = pool.get_client().await.unwrap();
413
414        let pid1: i32 = client1
415            .read_single_value_simple("select pg_backend_pid()")
416            .await;
417        let pid2: i32 = client2
418            .read_single_value_simple("select pg_backend_pid()")
419            .await;
420
421        // Close one client directly (bypasses pool return)
422        client1.close().await.unwrap();
423
424        // Drop the other so it returns to the pool's idle vec
425        drop(client2);
426        assert_eq!(pool.idle_connection_count(), 1);
427
428        // Close idle connections in the pool
429        pool.close().await.unwrap();
430        assert_eq!(pool.idle_connection_count(), 0);
431
432        // No sleep needed — Terminate triggers immediate backend cleanup
433        let mut checker = new_client(get_settings()).await.unwrap();
434        let count: i64 = checker
435            .read_single_value_simple(&format!(
436                "select count(*) from pg_stat_activity where pid in ({}, {})",
437                pid1, pid2
438            ))
439            .await;
440        assert_eq!(
441            count, 0,
442            "All connections should be closed after graceful shutdown"
443        );
444    }
445
446    #[tokio::test]
447    async fn pool_drop_closes_all_connections() {
448        let pid1: i32;
449        let pid2: i32;
450
451        {
452            let pool = TokioPostgresPool::new(TokioConnectionFactory, get_settings())
453                .await
454                .unwrap();
455
456            let mut client1 = pool.get_client().await.unwrap();
457            let mut client2 = pool.get_client().await.unwrap();
458
459            pid1 = client1
460                .read_single_value_simple("select pg_backend_pid()")
461                .await;
462            pid2 = client2
463                .read_single_value_simple("select pg_backend_pid()")
464                .await;
465
466            // Drop clients first (returns to idle vec), then drop pool (closes connections)
467            drop(client1);
468            drop(client2);
469        }
470        // pool is dropped here -- Arc refcount hits 0, idle connections are dropped, TCP streams close
471
472        // Small delay to let PostgreSQL clean up the backend processes
473        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
474
475        // Use a separate connection to verify the old connections are gone
476        let mut checker = new_client(get_settings()).await.unwrap();
477        let count: i64 = checker
478            .read_single_value_simple(&format!(
479                "select count(*) from pg_stat_activity where pid in ({}, {})",
480                pid1, pid2
481            ))
482            .await;
483        assert_eq!(
484            count, 0,
485            "All pool connections should be closed after pool drop"
486        );
487    }
488}