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    /// Close every idle connection.
103    pub async fn close(&self) {
104        let mut idle = self.inner.idle.lock().await;
105        while let Some((_, connection)) = idle.pop_front() {
106            connection.close().await;
107        }
108    }
109
110    /// Close idle connections belonging to a superseded credential, now.
111    ///
112    /// [`Pool::acquire`] already refuses to hand one out, so this is not needed
113    /// for correctness — it is for closing the window between a rotation and
114    /// the next request on a quiet pool, where an idle session opened with a
115    /// revoked account would otherwise sit there until somebody happened to
116    /// need it. Connections currently lent out are left alone; they are retired
117    /// when their borrower gives them back.
118    ///
119    /// Returns how many were closed.
120    pub async fn retire_superseded(&self) -> usize {
121        let generation = self.inner.driver.generation();
122        let mut idle = self.inner.idle.lock().await;
123
124        let mut keeping = VecDeque::with_capacity(idle.len());
125        let mut closed = 0;
126
127        while let Some((opened_under, connection)) = idle.pop_front() {
128            if opened_under == generation {
129                keeping.push_back((opened_under, connection));
130            } else {
131                connection.close().await;
132                closed += 1;
133            }
134        }
135
136        *idle = keeping;
137        closed
138    }
139}
140
141/// A connection borrowed from the pool, returned when dropped.
142pub struct PooledConnection {
143    connection: Option<Box<dyn DriverConnection>>,
144    /// The credential generation this connection was opened under.
145    generation: u64,
146    pool: Arc<Inner>,
147    /// Held for the lifetime of the borrow; releasing it lets another caller in.
148    _permit: tokio::sync::OwnedSemaphorePermit,
149}
150
151impl std::ops::Deref for PooledConnection {
152    type Target = dyn DriverConnection;
153
154    fn deref(&self) -> &(dyn DriverConnection + 'static) {
155        self.connection.as_deref().expect("connection is present until drop")
156    }
157}
158
159impl std::ops::DerefMut for PooledConnection {
160    fn deref_mut(&mut self) -> &mut (dyn DriverConnection + 'static) {
161        self.connection.as_deref_mut().expect("connection is present until drop")
162    }
163}
164
165impl Drop for PooledConnection {
166    fn drop(&mut self) {
167        let Some(connection) = self.connection.take() else { return };
168
169        // A broken connection, or one still inside a transaction, must not go
170        // back into rotation: the next borrower would inherit the mess.
171        if connection.is_broken() || connection.in_transaction() {
172            tokio::spawn(async move { connection.close().await });
173            return;
174        }
175
176        // A connection opened under a credential that has since been replaced
177        // is closed rather than returned: this is the "busy connections go when
178        // their borrower is finished" half of the rotation.
179        let pool = Arc::clone(&self.pool);
180        let generation = self.generation;
181        tokio::spawn(async move {
182            if generation != pool.driver.generation() {
183                connection.close().await;
184                return;
185            }
186            pool.idle.lock().await.push_back((generation, connection));
187        });
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::dialect::Postgres;
195    use crate::driver::BoxFuture;
196
197    /// A driver that hands out connections and counts how many it opened, with
198    /// a generation the test can move on by hand.
199    struct Counting {
200        opened: Arc<std::sync::atomic::AtomicUsize>,
201        closed: Arc<std::sync::atomic::AtomicUsize>,
202        generation: Arc<std::sync::atomic::AtomicU64>,
203    }
204
205    struct Nothing(Arc<std::sync::atomic::AtomicUsize>);
206
207    impl DriverConnection for Nothing {
208        fn query<'a>(
209            &'a mut self,
210            _sql: &'a str,
211            _params: &'a [crate::value::Value],
212        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
213            Box::pin(async { Err(Error::msg("not a real connection")) })
214        }
215
216        fn simple_query<'a>(
217            &'a mut self,
218            _sql: &'a str,
219        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
220            Box::pin(async { Err(Error::msg("not a real connection")) })
221        }
222
223        fn is_broken(&self) -> bool {
224            false
225        }
226
227        fn in_transaction(&self) -> bool {
228            false
229        }
230
231        fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
232            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
233            Box::pin(async {})
234        }
235    }
236
237    impl Driver for Counting {
238        fn dialect(&self) -> Arc<dyn Dialect> {
239            Arc::new(Postgres)
240        }
241
242        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
243            self.opened.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
244            let closed = Arc::clone(&self.closed);
245            Box::pin(async move { Ok(Box::new(Nothing(closed)) as Box<dyn DriverConnection>) })
246        }
247
248        fn describe(&self) -> String {
249            "test://counting".into()
250        }
251
252        fn generation(&self) -> u64 {
253            self.generation.load(std::sync::atomic::Ordering::Acquire)
254        }
255    }
256
257    fn counting() -> (Pool, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicU64>) {
258        let opened = Arc::new(std::sync::atomic::AtomicUsize::new(0));
259        let closed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
260        let generation = Arc::new(std::sync::atomic::AtomicU64::new(1));
261        let driver = Counting {
262            opened: Arc::clone(&opened),
263            closed: Arc::clone(&closed),
264            generation: Arc::clone(&generation),
265        };
266        (Pool::new(Arc::new(driver)), opened, closed, generation)
267    }
268
269    /// The drop handler returns the connection on a spawned task, so a test has
270    /// to let the runtime run before the pool has it back.
271    async fn settle() {
272        for _ in 0..8 {
273            tokio::task::yield_now().await;
274        }
275    }
276
277    #[tokio::test]
278    async fn a_connection_comes_back_and_is_reused() {
279        let (pool, opened, _, _) = counting();
280
281        drop(pool.acquire().await.unwrap());
282        settle().await;
283        drop(pool.acquire().await.unwrap());
284        settle().await;
285
286        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1, "the second borrow reused it");
287    }
288
289    #[tokio::test]
290    async fn an_idle_connection_from_a_rotated_credential_is_never_handed_out() {
291        // It would still work — a database authenticates once, at connect — and
292        // that is the danger: reusing it keeps access alive under an account
293        // the store has already revoked.
294        let (pool, opened, closed, generation) = counting();
295
296        drop(pool.acquire().await.unwrap());
297        settle().await;
298        assert_eq!(pool.idle_count().await, 1);
299
300        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
301
302        drop(pool.acquire().await.unwrap());
303        settle().await;
304
305        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 2, "a fresh connection");
306        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1, "the stale one was closed");
307    }
308
309    #[tokio::test]
310    async fn a_borrowed_connection_is_retired_when_it_comes_back() {
311        // The other half of the rotation: nothing in flight is interrupted, but
312        // a connection handed back after the credential changed does not go
313        // into the idle set.
314        let (pool, _, closed, generation) = counting();
315
316        let borrowed = pool.acquire().await.unwrap();
317        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
318        drop(borrowed);
319        settle().await;
320
321        assert_eq!(pool.idle_count().await, 0);
322        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1);
323    }
324
325    #[tokio::test]
326    async fn retiring_early_closes_the_stale_and_keeps_the_current() {
327        let (pool, _, closed, generation) = counting();
328
329        // Two idle connections from the current generation.
330        let first = pool.acquire().await.unwrap();
331        let second = pool.acquire().await.unwrap();
332        drop(first);
333        drop(second);
334        settle().await;
335        assert_eq!(pool.idle_count().await, 2);
336
337        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
338
339        assert_eq!(pool.retire_superseded().await, 2);
340        assert_eq!(pool.idle_count().await, 0);
341        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 2);
342
343        // And it leaves current ones alone.
344        drop(pool.acquire().await.unwrap());
345        settle().await;
346        assert_eq!(pool.retire_superseded().await, 0);
347        assert_eq!(pool.idle_count().await, 1);
348    }
349
350    #[tokio::test]
351    async fn a_pool_with_static_credentials_never_retires_anything() {
352        // Everything above must cost nothing for the ordinary case, where the
353        // generation is zero on both sides forever.
354        let (pool, opened, closed, _) = counting();
355
356        for _ in 0..5 {
357            drop(pool.acquire().await.unwrap());
358            settle().await;
359        }
360
361        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1);
362        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 0);
363    }
364
365    /// A driver that refuses to connect, so the pool can be tested with no
366    /// database anywhere near it.
367    struct Unreachable;
368
369    impl Driver for Unreachable {
370        fn dialect(&self) -> Arc<dyn Dialect> {
371            Arc::new(Postgres)
372        }
373
374        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
375            Box::pin(async { Err(Error::msg("nothing is listening")) })
376        }
377
378        fn describe(&self) -> String {
379            "test://unreachable".into()
380        }
381
382        fn max_connections(&self) -> usize {
383            3
384        }
385    }
386
387    #[tokio::test]
388    async fn a_pool_opens_nothing_until_it_is_used() {
389        let pool = Pool::new(Arc::new(Unreachable));
390        assert_eq!(pool.idle_count().await, 0);
391    }
392
393    #[tokio::test]
394    async fn acquiring_reports_the_drivers_failure() {
395        let pool = Pool::new(Arc::new(Unreachable));
396
397        let error = match pool.acquire().await {
398            Err(error) => error.to_string(),
399            Ok(_) => panic!("this driver cannot connect"),
400        };
401        assert!(error.contains("nothing is listening"), "{error}");
402    }
403
404    #[tokio::test]
405    async fn the_pool_carries_its_drivers_dialect() {
406        let pool = Pool::new(Arc::new(Unreachable));
407        assert_eq!(pool.dialect().name(), "postgres");
408    }
409}