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 std::sync::{Mutex, MutexGuard};
17use tokio::sync::Semaphore;
18
19struct Inner {
20    driver: Arc<dyn Driver>,
21    /// Each idle connection beside the credential generation it was opened
22    /// under, so one belonging to a rotated credential can be told apart from
23    /// one that is still current.
24    idle: Mutex<VecDeque<(u64, Box<dyn DriverConnection>)>>,
25    /// Bounds how many connections exist at once, including those in use.
26    permits: Arc<Semaphore>,
27}
28
29#[derive(Clone)]
30pub struct Pool {
31    inner: Arc<Inner>,
32}
33
34impl Inner {
35    /// The idle queue.
36    ///
37    /// **A synchronous lock, and that is the fix, not a detail.** Handing a
38    /// connection back has to finish before its permit is released, and the
39    /// hand-back happens in `Drop`, which cannot `await`. With an async lock
40    /// the only option was `tokio::spawn`, which let the next caller win the
41    /// permit, find this queue not yet refilled, and open another connection —
42    /// measured at four and five connections from a pool allowed two.
43    ///
44    /// No lock here is ever held across an `await`: every critical section is
45    /// a push, a pop or a drain, and anything slow — closing a connection —
46    /// happens after the guard is gone.
47    ///
48    /// A poisoned lock is recovered rather than propagated. Poisoning means a
49    /// thread panicked while holding it, and none of these sections can panic;
50    /// taking the whole pool down over somebody else's panic would turn one
51    /// failed request into every request failing.
52    fn idle(&self) -> MutexGuard<'_, VecDeque<(u64, Box<dyn DriverConnection>)>> {
53        self.idle.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
54    }
55}
56
57impl Pool {
58    /// Create a pool. No connection is opened until one is needed, so an
59    /// application still boots when the database is briefly unavailable.
60    pub fn new(driver: Arc<dyn Driver>) -> Self {
61        let permits = Arc::new(Semaphore::new(driver.max_connections().max(1)));
62        Pool { inner: Arc::new(Inner { driver, idle: Mutex::new(VecDeque::new()), permits }) }
63    }
64
65    /// Open one connection immediately, so a misconfiguration is reported at
66    /// boot rather than on the first request.
67    pub async fn verify(&self) -> Result<()> {
68        let mut connection = self.acquire().await?;
69        connection.simple_query("select 1").await?;
70        Ok(())
71    }
72
73    pub fn driver(&self) -> &Arc<dyn Driver> {
74        &self.inner.driver
75    }
76
77    pub fn dialect(&self) -> Arc<dyn Dialect> {
78        self.inner.driver.dialect()
79    }
80
81    /// Take a connection, opening one if none is idle.
82    pub async fn acquire(&self) -> Result<PooledConnection> {
83        let permit = Arc::clone(&self.inner.permits)
84            .acquire_owned()
85            .await
86            .map_err(|_| Error::msg("the database pool has been closed"))?;
87
88        let generation = self.inner.driver.generation();
89
90        // Discard anything opened under a credential that has since been
91        // replaced. Such a connection usually still *works* — a database
92        // authenticates once, at connect time — which is exactly why it has to
93        // be dropped deliberately: left alone it would keep an access granted
94        // by a revoked account alive for as long as the process runs.
95        loop {
96            let Some((opened_under, connection)) = self.inner.idle().pop_front() else {
97                break;
98            };
99
100            if opened_under == generation {
101                return Ok(PooledConnection {
102                    connection: Some(connection),
103                    generation,
104                    pool: Arc::clone(&self.inner),
105                    _permit: permit,
106                });
107            }
108
109            connection.close().await;
110        }
111
112        let connection = self.inner.driver.connect().await?;
113        Ok(PooledConnection {
114            connection: Some(connection),
115            generation,
116            pool: Arc::clone(&self.inner),
117            _permit: permit,
118        })
119    }
120
121    /// How many connections are currently idle. Used by tests and diagnostics.
122    pub async fn idle_count(&self) -> usize {
123        self.inner.idle().len()
124    }
125
126    /// How many sockets this pool is holding open: idle plus borrowed.
127    ///
128    /// The number that matters to the *server*, which is a different number
129    /// from the one the semaphore governs. A permit is held only while a
130    /// connection is borrowed, so the semaphore caps concurrent use; a
131    /// connection handed back sits in `idle` holding no permit and a very much
132    /// open socket. An application with one pool never has to care. One with a
133    /// pool per tenant does: fifty idle pools are five hundred sockets against
134    /// a server whose default limit is a hundred, while every semaphore reads
135    /// zero.
136    pub async fn open_count(&self) -> usize {
137        let borrowed = self
138            .inner
139            .driver
140            .max_connections()
141            .max(1)
142            .saturating_sub(self.inner.permits.available_permits());
143        self.inner.idle().len() + borrowed
144    }
145
146    /// Close up to `limit` idle connections, returning how many went.
147    ///
148    /// Only idle ones: a borrowed connection is in the middle of somebody's
149    /// query, and taking it away would turn a capacity problem into a failed
150    /// request. Freeing what is idle is enough — that is where a pool nobody
151    /// is using keeps its sockets.
152    pub async fn close_idle(&self, limit: usize) -> usize {
153        let mut closed = 0;
154        while closed < limit {
155            let Some((_, connection)) = self.inner.idle().pop_front() else { break };
156            connection.close().await;
157            closed += 1;
158        }
159        closed
160    }
161
162    /// Close every idle connection.
163    pub async fn close(&self) {
164        // Taken out first and closed afterwards: closing is network I/O, and
165        // nothing that slow may happen while the idle queue is locked.
166        let drained: Vec<_> = self.inner.idle().drain(..).collect();
167        for (_, connection) in drained {
168            connection.close().await;
169        }
170    }
171
172    /// Close idle connections belonging to a superseded credential, now.
173    ///
174    /// [`Pool::acquire`] already refuses to hand one out, so this is not needed
175    /// for correctness — it is for closing the window between a rotation and
176    /// the next request on a quiet pool, where an idle session opened with a
177    /// revoked account would otherwise sit there until somebody happened to
178    /// need it. Connections currently lent out are left alone; they are retired
179    /// when their borrower gives them back.
180    ///
181    /// Returns how many were closed.
182    pub async fn retire_superseded(&self) -> usize {
183        let generation = self.inner.driver.generation();
184
185        // Sorted under the lock, closed outside it.
186        let superseded: Vec<_> = {
187            let mut idle = self.inner.idle();
188            let mut superseded = Vec::new();
189            let mut keeping = VecDeque::with_capacity(idle.len());
190            for entry in idle.drain(..) {
191                if entry.0 == generation {
192                    keeping.push_back(entry);
193                } else {
194                    superseded.push(entry);
195                }
196            }
197            *idle = keeping;
198            superseded
199        };
200
201        let closed = superseded.len();
202        for (_, connection) in superseded {
203            connection.close().await;
204        }
205        closed
206    }
207}
208
209/// A connection borrowed from the pool, returned when dropped.
210pub struct PooledConnection {
211    connection: Option<Box<dyn DriverConnection>>,
212    /// The credential generation this connection was opened under.
213    generation: u64,
214    pool: Arc<Inner>,
215    /// Held for the lifetime of the borrow; releasing it lets another caller in.
216    _permit: tokio::sync::OwnedSemaphorePermit,
217}
218
219impl std::ops::Deref for PooledConnection {
220    type Target = dyn DriverConnection;
221
222    fn deref(&self) -> &(dyn DriverConnection + 'static) {
223        self.connection.as_deref().expect("connection is present until drop")
224    }
225}
226
227impl std::ops::DerefMut for PooledConnection {
228    fn deref_mut(&mut self) -> &mut (dyn DriverConnection + 'static) {
229        self.connection.as_deref_mut().expect("connection is present until drop")
230    }
231}
232
233impl Drop for PooledConnection {
234    fn drop(&mut self) {
235        let Some(connection) = self.connection.take() else { return };
236
237        // A broken connection, or one still inside a transaction, must not go
238        // back into rotation: the next borrower would inherit the mess.
239        if connection.is_broken() || connection.in_transaction() {
240            tokio::spawn(async move { connection.close().await });
241            return;
242        }
243
244        // A connection opened under a credential that has since been replaced
245        // is closed rather than returned: this is the "busy connections go when
246        // their borrower is finished" half of the rotation. Closing is the only
247        // part that needs a task — nothing is waiting on it.
248        if self.generation != self.pool.driver.generation() {
249            tokio::spawn(async move { connection.close().await });
250            return;
251        }
252
253        // **Back in the queue before this function returns**, and so before
254        // `_permit` is dropped with the rest of the fields. That ordering is
255        // the whole fix: the next caller cannot win this permit without also
256        // finding this connection waiting for it.
257        self.pool.idle().push_back((self.generation, connection));
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::dialect::Postgres;
265    use crate::driver::BoxFuture;
266
267    /// A driver that hands out connections and counts how many it opened, with
268    /// a generation the test can move on by hand.
269    struct Counting {
270        opened: Arc<std::sync::atomic::AtomicUsize>,
271        closed: Arc<std::sync::atomic::AtomicUsize>,
272        generation: Arc<std::sync::atomic::AtomicU64>,
273    }
274
275    struct Nothing(Arc<std::sync::atomic::AtomicUsize>);
276
277    impl DriverConnection for Nothing {
278        fn query<'a>(
279            &'a mut self,
280            _sql: &'a str,
281            _params: &'a [crate::value::Value],
282        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
283            Box::pin(async { Err(Error::msg("not a real connection")) })
284        }
285
286        fn simple_query<'a>(
287            &'a mut self,
288            _sql: &'a str,
289        ) -> BoxFuture<'a, Result<crate::driver::QueryResult>> {
290            Box::pin(async { Err(Error::msg("not a real connection")) })
291        }
292
293        fn is_broken(&self) -> bool {
294            false
295        }
296
297        fn in_transaction(&self) -> bool {
298            false
299        }
300
301        fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
302            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
303            Box::pin(async {})
304        }
305    }
306
307    impl Driver for Counting {
308        fn dialect(&self) -> Arc<dyn Dialect> {
309            Arc::new(Postgres)
310        }
311
312        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
313            self.opened.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
314            let closed = Arc::clone(&self.closed);
315            Box::pin(async move { Ok(Box::new(Nothing(closed)) as Box<dyn DriverConnection>) })
316        }
317
318        fn describe(&self) -> String {
319            "test://counting".into()
320        }
321
322        fn generation(&self) -> u64 {
323            self.generation.load(std::sync::atomic::Ordering::Acquire)
324        }
325    }
326
327    /// Counts connections like `Counting`, with a ceiling small enough that
328    /// exceeding it is unmistakable.
329    struct Capped {
330        opened: Arc<std::sync::atomic::AtomicUsize>,
331        closed: Arc<std::sync::atomic::AtomicUsize>,
332        max: usize,
333    }
334
335    impl Driver for Capped {
336        fn dialect(&self) -> Arc<dyn Dialect> {
337            Arc::new(Postgres)
338        }
339
340        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
341            self.opened.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
342            let closed = Arc::clone(&self.closed);
343            Box::pin(async move { Ok(Box::new(Nothing(closed)) as Box<dyn DriverConnection>) })
344        }
345
346        fn describe(&self) -> String {
347            "test://capped".into()
348        }
349
350        fn max_connections(&self) -> usize {
351            self.max
352        }
353    }
354
355    /// **The pool may never hold more connections than it was allowed.**
356    ///
357    /// Every connection here is healthy and outside any transaction, so each
358    /// one handed back must be reused rather than joined by a new one. On a
359    /// correct pool the number ever opened therefore cannot pass the ceiling,
360    /// however hard it is hammered.
361    ///
362    /// It did. `PooledConnection`'s `Drop` returned the connection through
363    /// `tokio::spawn`, but the permit — a field — was released as soon as
364    /// `drop` returned. The next caller could win that permit, find the idle
365    /// queue not yet refilled, and open another connection; the spawned task
366    /// then added the original to an idle queue with no bound. The permit
367    /// counted *borrowed* connections, and nothing counted the open ones.
368    ///
369    /// Invisible in any single test, and against a server merely wasteful —
370    /// until enough pools overshoot at once to reach the server's own
371    /// `max_connections`, which refuses whoever asks next. Found because
372    /// SQLite's `:memory:` makes a second connection a second, empty database.
373    #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
374    async fn a_pool_never_opens_more_connections_than_it_is_allowed() {
375        const CEILING: usize = 2;
376        let opened = Arc::new(std::sync::atomic::AtomicUsize::new(0));
377        let closed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
378        let pool = Pool::new(Arc::new(Capped {
379            opened: Arc::clone(&opened),
380            closed: Arc::clone(&closed),
381            max: CEILING,
382        }));
383
384        let mut workers = Vec::new();
385        for _ in 0..16 {
386            let pool = pool.clone();
387            workers.push(tokio::spawn(async move {
388                for _ in 0..200 {
389                    let connection = pool.acquire().await.expect("a connection");
390                    // Give the scheduler a chance to interleave the drop with
391                    // another task's acquire — the window the bug lives in.
392                    tokio::task::yield_now().await;
393                    drop(connection);
394                }
395            }));
396        }
397        for worker in workers {
398            worker.await.unwrap();
399        }
400
401        let opened = opened.load(std::sync::atomic::Ordering::SeqCst);
402        let closed = closed.load(std::sync::atomic::Ordering::SeqCst);
403        assert!(
404            opened <= CEILING,
405            "a pool allowed {CEILING} connections opened {opened} ({closed} closed) \
406             across 3,200 borrows"
407        );
408        assert!(
409            pool.open_count().await <= CEILING,
410            "{} connections are open in a pool allowed {CEILING}",
411            pool.open_count().await
412        );
413    }
414
415    fn counting() -> (Pool, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicU64>) {
416        let opened = Arc::new(std::sync::atomic::AtomicUsize::new(0));
417        let closed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
418        let generation = Arc::new(std::sync::atomic::AtomicU64::new(1));
419        let driver = Counting {
420            opened: Arc::clone(&opened),
421            closed: Arc::clone(&closed),
422            generation: Arc::clone(&generation),
423        };
424        (Pool::new(Arc::new(driver)), opened, closed, generation)
425    }
426
427    /// The drop handler returns the connection on a spawned task, so a test has
428    /// to let the runtime run before the pool has it back.
429    async fn settle() {
430        for _ in 0..8 {
431            tokio::task::yield_now().await;
432        }
433    }
434
435    #[tokio::test]
436    async fn a_connection_comes_back_and_is_reused() {
437        let (pool, opened, _, _) = counting();
438
439        drop(pool.acquire().await.unwrap());
440        settle().await;
441        drop(pool.acquire().await.unwrap());
442        settle().await;
443
444        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1, "the second borrow reused it");
445    }
446
447    #[tokio::test]
448    async fn an_idle_connection_from_a_rotated_credential_is_never_handed_out() {
449        // It would still work — a database authenticates once, at connect — and
450        // that is the danger: reusing it keeps access alive under an account
451        // the store has already revoked.
452        let (pool, opened, closed, generation) = counting();
453
454        drop(pool.acquire().await.unwrap());
455        settle().await;
456        assert_eq!(pool.idle_count().await, 1);
457
458        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
459
460        drop(pool.acquire().await.unwrap());
461        settle().await;
462
463        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 2, "a fresh connection");
464        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1, "the stale one was closed");
465    }
466
467    #[tokio::test]
468    async fn a_borrowed_connection_is_retired_when_it_comes_back() {
469        // The other half of the rotation: nothing in flight is interrupted, but
470        // a connection handed back after the credential changed does not go
471        // into the idle set.
472        let (pool, _, closed, generation) = counting();
473
474        let borrowed = pool.acquire().await.unwrap();
475        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
476        drop(borrowed);
477        settle().await;
478
479        assert_eq!(pool.idle_count().await, 0);
480        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 1);
481    }
482
483    #[tokio::test]
484    async fn retiring_early_closes_the_stale_and_keeps_the_current() {
485        let (pool, _, closed, generation) = counting();
486
487        // Two idle connections from the current generation.
488        let first = pool.acquire().await.unwrap();
489        let second = pool.acquire().await.unwrap();
490        drop(first);
491        drop(second);
492        settle().await;
493        assert_eq!(pool.idle_count().await, 2);
494
495        generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
496
497        assert_eq!(pool.retire_superseded().await, 2);
498        assert_eq!(pool.idle_count().await, 0);
499        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 2);
500
501        // And it leaves current ones alone.
502        drop(pool.acquire().await.unwrap());
503        settle().await;
504        assert_eq!(pool.retire_superseded().await, 0);
505        assert_eq!(pool.idle_count().await, 1);
506    }
507
508    #[tokio::test]
509    async fn a_pool_with_static_credentials_never_retires_anything() {
510        // Everything above must cost nothing for the ordinary case, where the
511        // generation is zero on both sides forever.
512        let (pool, opened, closed, _) = counting();
513
514        for _ in 0..5 {
515            drop(pool.acquire().await.unwrap());
516            settle().await;
517        }
518
519        assert_eq!(opened.load(std::sync::atomic::Ordering::SeqCst), 1);
520        assert_eq!(closed.load(std::sync::atomic::Ordering::SeqCst), 0);
521    }
522
523    /// A driver that refuses to connect, so the pool can be tested with no
524    /// database anywhere near it.
525    struct Unreachable;
526
527    impl Driver for Unreachable {
528        fn dialect(&self) -> Arc<dyn Dialect> {
529            Arc::new(Postgres)
530        }
531
532        fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
533            Box::pin(async { Err(Error::msg("nothing is listening")) })
534        }
535
536        fn describe(&self) -> String {
537            "test://unreachable".into()
538        }
539
540        fn max_connections(&self) -> usize {
541            3
542        }
543    }
544
545    #[tokio::test]
546    async fn a_pool_opens_nothing_until_it_is_used() {
547        let pool = Pool::new(Arc::new(Unreachable));
548        assert_eq!(pool.idle_count().await, 0);
549    }
550
551    #[tokio::test]
552    async fn acquiring_reports_the_drivers_failure() {
553        let pool = Pool::new(Arc::new(Unreachable));
554
555        let error = match pool.acquire().await {
556            Err(error) => error.to_string(),
557            Ok(_) => panic!("this driver cannot connect"),
558        };
559        assert!(error.contains("nothing is listening"), "{error}");
560    }
561
562    #[tokio::test]
563    async fn the_pool_carries_its_drivers_dialect() {
564        let pool = Pool::new(Arc::new(Unreachable));
565        assert_eq!(pool.dialect().name(), "postgres");
566    }
567}