Skip to main content

datumesh_store/
pool.rs

1//! A small connection pool: no global state, no background threads, and a per-connection
2//! cache of prepared statements (each statement is prepared once per connection, on first
3//! use, never when connecting).
4
5use std::collections::HashMap;
6use std::ops::{Deref, DerefMut};
7use std::sync::{Condvar, Mutex, MutexGuard};
8use std::time::{Duration, Instant};
9
10use postgres::{Client, Statement};
11use rustls::pki_types::pem::PemObject;
12use rustls::pki_types::CertificateDer;
13use rustls::RootCertStore;
14
15use crate::error::{Db, Error, Result};
16use crate::sql::Q;
17
18/// Opens a new connection.
19pub(crate) type Connector = Box<dyn Fn() -> core::result::Result<Client, postgres::Error> + Send + Sync>;
20
21/// How the pool behaves, and the roots TLS connections are verified against.
22///
23/// Builder style: `PoolOptions::new().max_size(4).tls_root_certificates_pem(&pem)?`.
24#[derive(Clone, PartialEq, Eq)]
25pub struct PoolOptions {
26    max_size: usize,
27    checkout_timeout: Duration,
28    pub(crate) webpki_roots: bool,
29    pub(crate) tls_roots: Vec<CertificateDer<'static>>,
30}
31
32impl std::fmt::Debug for PoolOptions {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("PoolOptions")
35            .field("max_size", &self.max_size)
36            .field("checkout_timeout", &self.checkout_timeout)
37            .field("webpki_roots", &self.webpki_roots)
38            .field("tls_root_certificates", &self.tls_roots.len())
39            .finish()
40    }
41}
42
43impl Default for PoolOptions {
44    fn default() -> PoolOptions {
45        PoolOptions::new()
46    }
47}
48
49impl PoolOptions {
50    /// At most 8 connections; wait at most 30 seconds for one; TLS connections verified
51    /// against the Mozilla roots (`webpki-roots`).
52    pub fn new() -> PoolOptions {
53        PoolOptions {
54            max_size: 8,
55            checkout_timeout: Duration::from_secs(30),
56            webpki_roots: true,
57            tls_roots: Vec::new(),
58        }
59    }
60
61    /// The most connections open at once (at least 1).
62    pub fn max_size(mut self, n: usize) -> PoolOptions {
63        self.max_size = n.max(1);
64        self
65    }
66
67    /// How long a call waits for a free connection before failing with
68    /// [`Error::PoolTimeout`] (reads: `None`).
69    pub fn checkout_timeout(mut self, d: Duration) -> PoolOptions {
70        self.checkout_timeout = d;
71        self
72    }
73
74    /// Adds the certificates of a PEM bundle (one or more `CERTIFICATE` sections; other
75    /// sections are ignored) to the roots TLS connections are verified against: a private
76    /// CA, or a server's self-signed certificate. Calls append. A bundle with no
77    /// certificate, or with one that does not parse, is [`Error::Config`].
78    pub fn tls_root_certificates_pem(mut self, pem: &[u8]) -> Result<PoolOptions> {
79        let mut check = RootCertStore::empty();
80        let mut n = 0;
81        for cert in CertificateDer::pem_slice_iter(pem) {
82            n += 1;
83            let cert = cert.map_err(|e| Error::Config(format!("root certificate {n} of the PEM bundle: {e}")))?;
84            check
85                .add(cert.clone())
86                .map_err(|e| Error::Config(format!("root certificate {n} of the PEM bundle: {e}")))?;
87            self.tls_roots.push(cert);
88        }
89        if n == 0 {
90            return Err(Error::Config("the PEM bundle holds no certificate".into()));
91        }
92        Ok(self)
93    }
94
95    /// Whether the Mozilla roots (`webpki-roots`, compiled in) are trusted too: yes by
96    /// default. `false` trusts only the roots added with
97    /// [`tls_root_certificates_pem`](Self::tls_root_certificates_pem) (a private CA).
98    pub fn tls_webpki_roots(mut self, yes: bool) -> PoolOptions {
99        self.webpki_roots = yes;
100        self
101    }
102}
103
104/// A connection and the statements prepared on it.
105pub(crate) struct Conn {
106    pub(crate) client: Client,
107    statements: HashMap<Q, Statement>,
108}
109
110impl Conn {
111    /// The prepared statement for `q`, preparing it on first use.
112    pub(crate) fn stmt(&mut self, q: Q) -> Result<Statement> {
113        if let Some(s) = self.statements.get(&q) {
114            return Ok(s.clone());
115        }
116        let s = self.client.prepare_typed(q.sql(), q.types()).db()?;
117        self.statements.insert(q, s.clone());
118        Ok(s)
119    }
120}
121
122struct State {
123    idle: Vec<Conn>,
124    open: usize,
125}
126
127pub(crate) struct Pool {
128    connect: Connector,
129    state: Mutex<State>,
130    freed: Condvar,
131    options: PoolOptions,
132}
133
134/// Settings every connection gets: the append-only triggers rely on READ COMMITTED.
135const SESSION: &str = "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED; \
136                       SET TIME ZONE 'UTC'; SET DateStyle = 'ISO, YMD'";
137
138impl Pool {
139    pub(crate) fn new(connect: Connector, options: PoolOptions) -> Pool {
140        Pool { connect, state: Mutex::new(State { idle: Vec::new(), open: 0 }), freed: Condvar::new(), options }
141    }
142
143    fn lock(&self) -> MutexGuard<'_, State> {
144        // A panic while holding the lock cannot leave State inconsistent (two fields, each
145        // updated in one step), so a poisoned lock is still usable.
146        self.state.lock().unwrap_or_else(|e| e.into_inner())
147    }
148
149    /// A free connection, a new one if fewer than `max_size` are open, or one freed within
150    /// the checkout timeout.
151    pub(crate) fn get(&self) -> Result<Pooled<'_>> {
152        let deadline = Instant::now() + self.options.checkout_timeout;
153        let mut state = self.lock();
154        loop {
155            while let Some(conn) = state.idle.pop() {
156                if conn.client.is_closed() {
157                    state.open -= 1;
158                    continue;
159                }
160                return Ok(Pooled { pool: self, conn: Some(conn) });
161            }
162            if state.open < self.options.max_size {
163                state.open += 1;
164                drop(state);
165                return match self.open() {
166                    Ok(conn) => Ok(Pooled { pool: self, conn: Some(conn) }),
167                    Err(e) => {
168                        self.lock().open -= 1;
169                        self.freed.notify_one();
170                        Err(e)
171                    }
172                };
173            }
174            let now = Instant::now();
175            if now >= deadline {
176                return Err(Error::PoolTimeout);
177            }
178            state = self.freed.wait_timeout(state, deadline - now).unwrap_or_else(|e| e.into_inner()).0;
179        }
180    }
181
182    fn open(&self) -> Result<Conn> {
183        let mut client = (self.connect)().db()?;
184        client.batch_execute(SESSION).db()?;
185        Ok(Conn { client, statements: HashMap::new() })
186    }
187
188    fn put_back(&self, conn: Conn) {
189        let mut state = self.lock();
190        if conn.client.is_closed() {
191            state.open -= 1;
192        } else {
193            state.idle.push(conn);
194        }
195        drop(state);
196        self.freed.notify_one();
197    }
198}
199
200/// A connection checked out of the pool; returned to it when dropped (or discarded if the
201/// server closed it).
202pub(crate) struct Pooled<'a> {
203    pool: &'a Pool,
204    conn: Option<Conn>,
205}
206
207impl Deref for Pooled<'_> {
208    type Target = Conn;
209    fn deref(&self) -> &Conn {
210        // Some from creation until drop.
211        self.conn.as_ref().expect("connection present until drop")
212    }
213}
214
215impl DerefMut for Pooled<'_> {
216    fn deref_mut(&mut self) -> &mut Conn {
217        self.conn.as_mut().expect("connection present until drop")
218    }
219}
220
221impl Drop for Pooled<'_> {
222    fn drop(&mut self) {
223        if let Some(conn) = self.conn.take() {
224            self.pool.put_back(conn);
225        }
226    }
227}