Skip to main content

rustlavel_db/
pool.rs

1//! The connection pool.
2//!
3//! Opening a connection costs a TCP handshake plus authentication, whatever the
4//! database, which is far too much to pay per request. The pool keeps a small
5//! set alive and hands them out, discarding any that broke or that a handler
6//! left inside a transaction.
7//!
8//! It knows nothing about any particular database: it holds a [`Driver`], and
9//! the driver knows the protocol.
10
11use crate::dialect::Dialect;
12use crate::driver::{Driver, DriverConnection};
13use rustlavel_core::{Error, Result};
14use std::collections::VecDeque;
15use std::sync::Arc;
16use tokio::sync::{Mutex, Semaphore};
17
18struct Inner {
19    driver: Arc<dyn Driver>,
20    /// Each idle connection beside the credential generation it was opened
21    /// under, so one belonging to a rotated credential can be told apart from
22    /// one that is still current.
23    idle: Mutex<VecDeque<(u64, Box<dyn DriverConnection>)>>,
24    /// Bounds how many connections exist at once, including those in use.
25    permits: Arc<Semaphore>,
26}
27
28#[derive(Clone)]
29pub struct Pool {
30    inner: Arc<Inner>,
31}
32
33impl Pool {
34    /// Create a pool. No connection is opened until one is needed, so an
35    /// application still boots when the database is briefly unavailable.
36    pub fn new(driver: Arc<dyn Driver>) -> Self {
37        let permits = Arc::new(Semaphore::new(driver.max_connections().max(1)));
38        Pool { inner: Arc::new(Inner { driver, idle: Mutex::new(VecDeque::new()), permits }) }
39    }
40
41    /// Open one connection immediately, so a misconfiguration is reported at
42    /// boot rather than on the first request.
43    pub async fn verify(&self) -> Result<()> {
44        let mut connection = self.acquire().await?;
45        connection.simple_query("select 1").await?;
46        Ok(())
47    }
48
49    pub fn driver(&self) -> &Arc<dyn Driver> {
50        &self.inner.driver
51    }
52
53    pub fn dialect(&self) -> Arc<dyn Dialect> {
54        self.inner.driver.dialect()
55    }
56
57    /// Take a connection, opening one if none is idle.
58    pub async fn acquire(&self) -> Result<PooledConnection> {
59        let permit = Arc::clone(&self.inner.permits)
60            .acquire_owned()
61            .await
62            .map_err(|_| Error::msg("the database pool has been closed"))?;
63
64        let generation = self.inner.driver.generation();
65
66        // Discard anything opened under a credential that has since been
67        // replaced. Such a connection usually still *works* — a database
68        // authenticates once, at connect time — which is exactly why it has to
69        // be dropped deliberately: left alone it would keep an access granted
70        // by a revoked account alive for as long as the process runs.
71        loop {
72            let Some((opened_under, connection)) = self.inner.idle.lock().await.pop_front() else {
73                break;
74            };
75
76            if opened_under == generation {
77                return Ok(PooledConnection {
78                    connection: Some(connection),
79                    generation,
80                    pool: Arc::clone(&self.inner),
81                    _permit: permit,
82                });
83            }
84
85            connection.close().await;
86        }
87
88        let connection = self.inner.driver.connect().await?;
89        Ok(PooledConnection {
90            connection: Some(connection),
91            generation,
92            pool: Arc::clone(&self.inner),
93            _permit: permit,
94        })
95    }
96
97    /// How many connections are currently idle. Used by tests and diagnostics.
98    pub async fn idle_count(&self) -> usize {
99        self.inner.idle.lock().await.len()
100    }
101
102    /// How many sockets this pool is holding open: idle plus borrowed.
103    ///
104    /// The number that matters to the *server*, which is a different number
105    /// from the one the semaphore governs. A permit is held only while a
106    /// connection is borrowed, so the semaphore caps concurrent use; a
107    /// connection handed back sits in `idle` holding no permit and a very much
108    /// open socket. An application with one pool never has to care. One with a
109    /// pool per tenant does: fifty idle pools are five hundred sockets against
110    /// a server whose default limit is a hundred, while every semaphore reads
111    /// zero.
112    pub async fn open_count(&self) -> usize {
113        let borrowed = self
114            .inner
115            .driver
116            .max_connections()
117            .max(1)
118            .saturating_sub(self.inner.permits.available_permits());
119        self.inner.idle.lock().await.len() + borrowed
120    }
121
122    /// Close up to `limit` idle connections, returning how many went.
123    ///
124    /// Only idle ones: a borrowed connection is in the middle of somebody's
125    /// query, and taking it away would turn a capacity problem into a failed
126    /// request. Freeing what is idle is enough — that is where a pool nobody
127    /// is using keeps its sockets.
128    pub async fn close_idle(&self, limit: usize) -> usize {
129        let mut closed = 0;
130        while closed < limit {
131            let Some((_, connection)) = self.inner.idle.lock().await.pop_front() else { break };
132            connection.close().await;
133            closed += 1;
134        }
135        closed
136    }
137
138    /// Close every idle connection.
139    pub async fn close(&self) {
140        let mut idle = self.inner.idle.lock().await;
141        while let Some((_, connection)) = idle.pop_front() {
142            connection.close().await;
143        }
144    }
145
146    /// Close idle connections belonging to a superseded credential, now.
147    ///
148    /// [`Pool::acquire`] already refuses to hand one out, so this is not needed
149    /// for correctness — it is for closing the window between a rotation and
150    /// the next request on a quiet pool, where an idle session opened with a
151    /// revoked account would otherwise sit there until somebody happened to
152    /// need it. Connections currently lent out are left alone; they are retired
153    /// when their borrower gives them back.
154    ///
155    /// Returns how many were closed.
156    pub async fn retire_superseded(&self) -> usize {
157        let generation = self.inner.driver.generation();
158        let mut idle = self.inner.idle.lock().await;
159
160        let mut keeping = VecDeque::with_capacity(idle.len());
161        let mut closed = 0;
162
163        while let Some((opened_under, connection)) = idle.pop_front() {
164            if opened_under == generation {
165                keeping.push_back((opened_under, connection));
166            } else {
167                connection.close().await;
168                closed += 1;
169            }
170        }
171
172        *idle = keeping;
173        closed
174    }
175}
176
177/// A connection borrowed from the pool, returned when dropped.
178pub struct PooledConnection {
179    connection: Option<Box<dyn DriverConnection>>,
180    /// The credential generation this connection was opened under.
181    generation: u64,
182    pool: Arc<Inner>,
183    /// Held for the lifetime of the borrow; releasing it lets another caller in.
184    _permit: tokio::sync::OwnedSemaphorePermit,
185}
186
187impl std::ops::Deref for PooledConnection {
188    type Target = dyn DriverConnection;
189
190    fn deref(&self) -> &(dyn DriverConnection + 'static) {
191        self.connection.as_deref().expect("connection is present until drop")
192    }
193}
194
195impl std::ops::DerefMut for PooledConnection {
196    fn deref_mut(&mut self) -> &mut (dyn DriverConnection + 'static) {
197        self.connection.as_deref_mut().expect("connection is present until drop")
198    }
199}
200
201impl Drop for PooledConnection {
202    fn drop(&mut self) {
203        let Some(connection) = self.connection.take() else { return };
204
205        // A broken connection, or one still inside a transaction, must not go
206        // back into rotation: the next borrower would inherit the mess.
207        if connection.is_broken() || connection.in_transaction() {
208            tokio::spawn(async move { connection.close().await });
209            return;
210        }
211
212        // A connection opened under a credential that has since been replaced
213        // is closed rather than returned: this is the "busy connections go when
214        // their borrower is finished" half of the rotation.
215        let pool = Arc::clone(&self.pool);
216        let generation = self.generation;
217        tokio::spawn(async move {
218            if generation != pool.driver.generation() {
219                connection.close().await;
220                return;
221            }
222            pool.idle.lock().await.push_back((generation, connection));
223        });
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::dialect::Postgres;
231    use crate::driver::BoxFuture;
232
233    /// A driver that hands out connections and counts how many it opened, with
234    /// a generation the test can move on by hand.
235    struct Counting {
236        opened: Arc<std::sync::atomic::AtomicUsize>,
237        closed: Arc<std::sync::atomic::AtomicUsize>,
238        generation: Arc<std::sync::atomic::AtomicU64>,
239    }
240
241    struct Nothing(Arc<std::sync::atomic::AtomicUsize>);
242
243    impl DriverConnection for Nothing {
244        fn query<'a>(
245            &'a mut self,
246            _sql: &'a str,
247            _params: &'a [crate::value::Value],
248        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
249            Box::pin(async { Err(Error::msg("not a real connection")) })
250        }
251
252        fn simple_query<'a>(
253            &'a mut self,
254            _sql: &'a str,
255        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
256            Box::pin(async { Err(Error::msg("not a real connection")) })
257        }
258
259        fn is_broken(&self) -> bool {
260            false
261        }
262
263        fn in_transaction(&self) -> bool {
264            false
265        }
266
267        fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
268            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
269            Box::pin(async {})
270        }
271    }
272
273    impl Driver for Counting {
274        fn dialect(&self) -> Arc<dyn Dialect> {
275            Arc::new(Postgres)
276        }
277
278        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
279            self.opened.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
280            let closed = Arc::clone(&self.closed);
281            Box::pin(async move { Ok(Box::new(Nothing(closed)) as Box<dyn DriverConnection>) })
282        }
283
284        fn describe(&self) -> String {
285            "test://counting".into()
286        }
287
288        fn generation(&self) -> u64 {
289            self.generation.load(std::sync::atomic::Ordering::Acquire)
290        }
291    }
292
293    fn counting() -> (Pool, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicU64>) {
294        let opened = Arc::new(std::sync::atomic::AtomicUsize::new(0));
295        let closed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
296        let generation = Arc::new(std::sync::atomic::AtomicU64::new(1));
297        let driver = Counting {
298            opened: Arc::clone(&opened),
299            closed: Arc::clone(&closed),
300            generation: Arc::clone(&generation),
301        };
302        (Pool::new(Arc::new(driver)), opened, closed, generation)
303    }
304
305    /// The drop handler returns the connection on a spawned task, so a test has
306    /// to let the runtime run before the pool has it back.
307    async fn settle() {
308        for _ in 0..8 {
309            tokio::task::yield_now().await;
310        }
311    }
312
313    #[tokio::test]
314    async fn a_connection_comes_back_and_is_reused() {
315        let (pool, opened, _, _) = counting();
316
317        drop(pool.acquire().await.unwrap());
318        settle().await;
319        drop(pool.acquire().await.unwrap());
320        settle().await;
321
322        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1, "the second borrow reused it");
323    }
324
325    #[tokio::test]
326    async fn an_idle_connection_from_a_rotated_credential_is_never_handed_out() {
327        // It would still work — a database authenticates once, at connect — and
328        // that is the danger: reusing it keeps access alive under an account
329        // the store has already revoked.
330        let (pool, opened, closed, generation) = counting();
331
332        drop(pool.acquire().await.unwrap());
333        settle().await;
334        assert_eq!(pool.idle_count().await, 1);
335
336        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
337
338        drop(pool.acquire().await.unwrap());
339        settle().await;
340
341        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 2, "a fresh connection");
342        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1, "the stale one was closed");
343    }
344
345    #[tokio::test]
346    async fn a_borrowed_connection_is_retired_when_it_comes_back() {
347        // The other half of the rotation: nothing in flight is interrupted, but
348        // a connection handed back after the credential changed does not go
349        // into the idle set.
350        let (pool, _, closed, generation) = counting();
351
352        let borrowed = pool.acquire().await.unwrap();
353        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
354        drop(borrowed);
355        settle().await;
356
357        assert_eq!(pool.idle_count().await, 0);
358        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1);
359    }
360
361    #[tokio::test]
362    async fn retiring_early_closes_the_stale_and_keeps_the_current() {
363        let (pool, _, closed, generation) = counting();
364
365        // Two idle connections from the current generation.
366        let first = pool.acquire().await.unwrap();
367        let second = pool.acquire().await.unwrap();
368        drop(first);
369        drop(second);
370        settle().await;
371        assert_eq!(pool.idle_count().await, 2);
372
373        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
374
375        assert_eq!(pool.retire_superseded().await, 2);
376        assert_eq!(pool.idle_count().await, 0);
377        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 2);
378
379        // And it leaves current ones alone.
380        drop(pool.acquire().await.unwrap());
381        settle().await;
382        assert_eq!(pool.retire_superseded().await, 0);
383        assert_eq!(pool.idle_count().await, 1);
384    }
385
386    #[tokio::test]
387    async fn a_pool_with_static_credentials_never_retires_anything() {
388        // Everything above must cost nothing for the ordinary case, where the
389        // generation is zero on both sides forever.
390        let (pool, opened, closed, _) = counting();
391
392        for _ in 0..5 {
393            drop(pool.acquire().await.unwrap());
394            settle().await;
395        }
396
397        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1);
398        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 0);
399    }
400
401    /// A driver that refuses to connect, so the pool can be tested with no
402    /// database anywhere near it.
403    struct Unreachable;
404
405    impl Driver for Unreachable {
406        fn dialect(&self) -> Arc<dyn Dialect> {
407            Arc::new(Postgres)
408        }
409
410        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
411            Box::pin(async { Err(Error::msg("nothing is listening")) })
412        }
413
414        fn describe(&self) -> String {
415            "test://unreachable".into()
416        }
417
418        fn max_connections(&self) -> usize {
419            3
420        }
421    }
422
423    #[tokio::test]
424    async fn a_pool_opens_nothing_until_it_is_used() {
425        let pool = Pool::new(Arc::new(Unreachable));
426        assert_eq!(pool.idle_count().await, 0);
427    }
428
429    #[tokio::test]
430    async fn acquiring_reports_the_drivers_failure() {
431        let pool = Pool::new(Arc::new(Unreachable));
432
433        let error = match pool.acquire().await {
434            Err(error) => error.to_string(),
435            Ok(_) => panic!("this driver cannot connect"),
436        };
437        assert!(error.contains("nothing is listening"), "{error}");
438    }
439
440    #[tokio::test]
441    async fn the_pool_carries_its_drivers_dialect() {
442        let pool = Pool::new(Arc::new(Unreachable));
443        assert_eq!(pool.dialect().name(), "postgres");
444    }
445}