Skip to main content

better_duck_core/asynchronous/
pool.rs

1use crate::{
2    connection::Connection,
3    error::{Error, Result},
4    pool::{Pool, PoolState},
5    result_set::ResultSet,
6    types::{appendable::AppendAble, value::DuckValue},
7};
8
9/// An async facade over a [`Pool`].
10///
11/// [`with`](AsyncPool::with) is the primitive: it checks a connection out of the
12/// pool and runs a closure against it on a blocking thread, releasing the
13/// checkout when the closure returns. This is deliberately the only way to touch
14/// a pooled connection — holding a long-lived pooled-connection guard across
15/// `.await` points is the classic way to deadlock an async pool, since it pins a
16/// pool slot for the entire suspension.
17#[derive(Clone)]
18pub struct AsyncPool {
19    inner: Pool,
20}
21
22impl AsyncPool {
23    /// Wraps an existing [`Pool`] for async use.
24    pub fn new(pool: Pool) -> AsyncPool {
25        AsyncPool { inner: pool }
26    }
27
28    /// Returns the underlying [`Pool`].
29    pub fn pool(&self) -> &Pool {
30        &self.inner
31    }
32
33    /// Returns the pool's current idle/active connection counts.
34    pub async fn state(&self) -> PoolState {
35        self.inner.state()
36    }
37
38    /// Checks a connection out of the pool and runs `f` against it on a blocking
39    /// thread. The checkout is released when `f` returns.
40    ///
41    /// This is the correct place to run a transaction: a transaction must not
42    /// span an `.await` point.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if checkout times out, or if the closure returns one.
47    pub async fn with<F, T>(
48        &self,
49        f: F,
50    ) -> Result<T>
51    where
52        F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
53        T: Send + 'static,
54    {
55        let pool = self.inner.clone();
56        tokio::task::spawn_blocking(move || {
57            let mut conn = pool.get().map_err(|e| Error::Pool(e.to_string()))?;
58            f(&mut conn)
59        })
60        .await
61        .map_err(|e| Error::BackgroundTaskFailed(e.to_string()))?
62    }
63
64    /// Executes one or more SQL statements separated by semicolons on a pooled
65    /// connection.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if checkout times out or any statement fails to execute.
70    pub async fn execute_batch<S>(
71        &self,
72        sql: S,
73    ) -> Result<()>
74    where
75        S: Into<String> + Send,
76    {
77        let sql = sql.into();
78        self.with(move |conn| conn.execute_batch(&sql)).await
79    }
80
81    /// Prepares and executes a SQL statement on a pooled connection, materializing
82    /// the result.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if checkout times out or the statement fails.
87    pub async fn execute<S>(
88        &self,
89        sql: S,
90    ) -> Result<ResultSet>
91    where
92        S: Into<String> + Send,
93    {
94        let sql = sql.into();
95        self.with(move |conn| conn.execute(&sql)?.materialize()).await
96    }
97
98    /// Prepares and executes a parameterized SQL statement on a pooled connection,
99    /// materializing the result.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if checkout times out, or preparation, binding, or
104    /// execution fails.
105    pub async fn execute_with<S>(
106        &self,
107        sql: S,
108        binds: Vec<DuckValue>,
109    ) -> Result<ResultSet>
110    where
111        S: Into<String> + Send,
112    {
113        let sql = sql.into();
114        self.with(move |conn| {
115            let mut owned = binds;
116            let mut refs: Vec<&mut dyn AppendAble> =
117                owned.iter_mut().map(|v| v as &mut dyn AppendAble).collect();
118            conn.execute_with(&sql, &mut refs)?.materialize()
119        })
120        .await
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::pool::DuckDbConnectionManager;
128
129    fn make_pool(max_size: u32) -> AsyncPool {
130        let manager = DuckDbConnectionManager::memory().unwrap();
131        let pool = Pool::builder().max_size(max_size).build(manager).unwrap();
132        AsyncPool::new(pool)
133    }
134
135    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
136    async fn concurrent_pool_queries() {
137        let pool = make_pool(4);
138        pool.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
139
140        let mut handles = Vec::new();
141        for _ in 0..32 {
142            let p = pool.clone();
143            handles.push(tokio::spawn(async move {
144                p.execute_with("INSERT INTO t VALUES ($1)", vec![DuckValue::Int(1)]).await.unwrap();
145            }));
146        }
147        for h in handles {
148            h.await.unwrap();
149        }
150
151        let result = pool.execute("SELECT count(*) AS c FROM t").await.unwrap();
152        match result.rows()[0].get("c").unwrap() {
153            DuckValue::BigInt(n) => assert_eq!(*n, 32),
154            other => panic!("expected BigInt, got {other:?}"),
155        }
156    }
157
158    #[tokio::test]
159    async fn pool_with_runs_transaction() {
160        let pool = make_pool(2);
161        pool.execute_batch("CREATE TABLE t (id INTEGER)").await.unwrap();
162        pool.with(|conn| {
163            conn.execute_batch("BEGIN")?;
164            conn.execute_batch("INSERT INTO t VALUES (1)")?;
165            conn.execute_batch("COMMIT")?;
166            Ok(())
167        })
168        .await
169        .unwrap();
170        let result = pool.execute("SELECT count(*) AS c FROM t").await.unwrap();
171        match result.rows()[0].get("c").unwrap() {
172            DuckValue::BigInt(n) => assert_eq!(*n, 1),
173            other => panic!("expected BigInt, got {other:?}"),
174        }
175    }
176
177    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
178    async fn pool_does_not_block_the_executor() {
179        let pool = make_pool(1);
180        let slow = {
181            let p = pool.clone();
182            tokio::spawn(async move {
183                p.with(|conn| {
184                    conn.execute_batch("SELECT 1")?;
185                    std::thread::sleep(std::time::Duration::from_millis(200));
186                    Ok(())
187                })
188                .await
189                .unwrap();
190            })
191        };
192        let start = tokio::time::Instant::now();
193        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
194        assert!(start.elapsed() < std::time::Duration::from_millis(150));
195        slow.await.unwrap();
196    }
197}