use std::collections::HashMap;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use commonmeta::rusqlite::Connection;
use crate::error::AppError;
const MAX_IDLE: usize = 64;
pub struct Pool {
path: PathBuf,
idle: Mutex<Vec<Connection>>,
}
pub struct Pooled {
conn: Option<Connection>,
pool: Arc<Pool>,
}
impl Deref for Pooled {
type Target = Connection;
fn deref(&self) -> &Connection {
self.conn
.as_ref()
.expect("a pooled connection is held until drop")
}
}
impl Drop for Pooled {
fn drop(&mut self) {
let Some(conn) = self.conn.take() else {
return;
};
let mut idle = self.pool.idle.lock().unwrap_or_else(|e| e.into_inner());
if idle.len() < MAX_IDLE {
idle.push(conn);
}
}
}
impl Pool {
fn new(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
idle: Mutex::new(Vec::new()),
}
}
fn take(self: &Arc<Self>) -> Result<Pooled, AppError> {
let idle = self.idle.lock().unwrap_or_else(|e| e.into_inner()).pop();
let conn = match idle {
Some(conn) => conn,
None => open(&self.path)?,
};
Ok(Pooled {
conn: Some(conn),
pool: self.clone(),
})
}
#[cfg(test)]
fn idle(&self) -> usize {
self.idle.lock().unwrap().len()
}
}
fn pools() -> &'static Mutex<HashMap<PathBuf, Arc<Pool>>> {
static POOLS: OnceLock<Mutex<HashMap<PathBuf, Arc<Pool>>>> = OnceLock::new();
POOLS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn get(path: &Path) -> Result<Pooled, AppError> {
let pool = {
let mut pools = pools().lock().unwrap_or_else(|e| e.into_inner());
pools
.entry(path.to_path_buf())
.or_insert_with(|| Arc::new(Pool::new(path)))
.clone()
};
pool.take()
}
fn open(path: &Path) -> Result<Connection, AppError> {
let conn = commonmeta::open_readonly(path)
.map_err(|e| AppError::Internal(format!("sqlite open '{}': {e}", path.display())))?;
conn.busy_timeout(std::time::Duration::from_secs(30))
.map_err(|e| {
AppError::Internal(format!("sqlite busy_timeout '{}': {e}", path.display()))
})?;
conn.execute_batch(
"PRAGMA mmap_size = 549755813888; PRAGMA cache_size = -8192; PRAGMA temp_store = MEMORY;",
)
.ok();
Ok(conn)
}
#[cfg(test)]
mod tests {
use super::*;
fn store() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("store.sqlite3");
Connection::open(&path)
.unwrap()
.execute_batch("CREATE TABLE t (x INTEGER); INSERT INTO t VALUES (1);")
.unwrap();
(dir, path)
}
#[test]
fn a_connection_is_given_back_and_taken_again() {
let (_dir, path) = store();
let first = get(&path).unwrap();
first.execute_batch("PRAGMA cache_size = -1234").unwrap();
let pool = first.pool.clone();
drop(first);
assert_eq!(pool.idle(), 1);
let again = get(&path).unwrap();
let mark: i64 = again
.query_row("PRAGMA cache_size", [], |r| r.get(0))
.unwrap();
assert_eq!(mark, -1234);
assert_eq!(pool.idle(), 0);
let x: i64 = again
.query_row("SELECT x FROM t", [], |r| r.get(0))
.unwrap();
assert_eq!(x, 1);
}
#[test]
fn a_burst_opens_more_and_the_pool_keeps_at_most_its_cap() {
let (_dir, path) = store();
let held: Vec<Pooled> = (0..MAX_IDLE + 3).map(|_| get(&path).unwrap()).collect();
let pool = held[0].pool.clone();
drop(held);
assert_eq!(pool.idle(), MAX_IDLE);
}
#[test]
fn a_pooled_connection_is_read_only_and_waits_for_locks() {
let (_dir, path) = store();
let conn = get(&path).unwrap();
assert!(conn.execute("INSERT INTO t VALUES (2)", []).is_err());
let timeout: i64 = conn
.query_row("PRAGMA busy_timeout", [], |r| r.get(0))
.unwrap();
assert_eq!(timeout, 30_000);
}
}