use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use super::connection::{Database, DbError};
pub struct Pool {
path: PathBuf,
idle: parking_lot::Mutex<Vec<Database>>,
keep: usize,
schema: AtomicBool,
applying: parking_lot::Mutex<()>,
}
const KEEP_IDLE: usize = 32;
pub fn shared() -> &'static Pool {
static POOL: std::sync::OnceLock<Pool> = std::sync::OnceLock::new();
POOL.get_or_init(|| Pool::new(crate::config::db_path()))
}
impl Pool {
pub fn new(path: PathBuf) -> Self {
Self {
path,
idle: parking_lot::Mutex::new(Vec::new()),
keep: KEEP_IDLE,
schema: AtomicBool::new(false),
applying: parking_lot::Mutex::new(()),
}
}
fn ensure_schema(&self) -> Result<(), DbError> {
if self.schema.load(Ordering::Acquire) {
return Ok(());
}
let _applying = self.applying.lock();
if self.schema.load(Ordering::Acquire) {
return Ok(());
}
Database::open(&self.path)?;
self.schema.store(true, Ordering::Release);
Ok(())
}
pub fn get(&self) -> Result<Handle<'_>, DbError> {
self.ensure_schema()?;
let pooled = self.idle.lock().pop();
let db = match pooled {
Some(db) => db,
None => Database::open_existing(&self.path)?,
};
Ok(Handle {
db: Some(db),
pool: self,
})
}
pub fn path(&self) -> &Path {
&self.path
}
fn put_back(&self, db: Database) {
let mut idle = self.idle.lock();
if idle.len() < self.keep {
idle.push(db);
}
}
}
pub struct Handle<'a> {
db: Option<Database>,
pool: &'a Pool,
}
impl Deref for Handle<'_> {
type Target = Database;
fn deref(&self) -> &Database {
self.db.as_ref().expect("a handle holds its connection")
}
}
impl Drop for Handle<'_> {
fn drop(&mut self) {
if let Some(db) = self.db.take() {
self.pool.put_back(db);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pool() -> (tempfile::TempDir, Pool) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("koan.db");
(dir, Pool::new(path))
}
#[test]
fn the_first_connection_applies_the_schema() {
let (_dir, pool) = pool();
let db = pool.get().unwrap();
let tracks: i64 = db
.conn
.query_row("SELECT count(*) FROM tracks", [], |r| r.get(0))
.expect("the schema should be there");
assert_eq!(tracks, 0);
}
#[test]
fn a_connection_comes_back_and_is_reused() {
let (_dir, pool) = pool();
{
let db = pool.get().unwrap();
db.conn.execute_batch("SELECT 1").unwrap();
}
assert_eq!(pool.idle.lock().len(), 1, "returned on drop");
{
let _db = pool.get().unwrap();
assert_eq!(pool.idle.lock().len(), 0, "handed back out");
}
assert_eq!(pool.idle.lock().len(), 1);
}
#[test]
fn concurrent_borrowers_get_their_own() {
let (_dir, pool) = pool();
let first = pool.get().unwrap();
let second = pool.get().unwrap();
first.conn.execute_batch("SELECT 1").unwrap();
second.conn.execute_batch("SELECT 1").unwrap();
drop(first);
drop(second);
assert_eq!(pool.idle.lock().len(), 2, "both kept for next time");
}
#[test]
fn idle_connections_are_capped() {
let (_dir, mut pool) = pool();
pool.keep = 2;
let handles: Vec<_> = (0..6).map(|_| pool.get().unwrap()).collect();
assert_eq!(pool.idle.lock().len(), 0, "all of them are out");
drop(handles);
assert_eq!(pool.idle.lock().len(), 2, "the rest closed on return");
}
#[test]
fn a_pooled_connection_sees_what_another_wrote() {
let (_dir, pool) = pool();
{
let db = pool.get().unwrap();
db.conn
.execute_batch("CREATE TABLE probe (v INTEGER); INSERT INTO probe VALUES (7)")
.unwrap();
}
let db = pool.get().unwrap();
let v: i64 = db
.conn
.query_row("SELECT v FROM probe", [], |r| r.get(0))
.unwrap();
assert_eq!(v, 7);
}
}