use crate::core::db;
use crate::core::error::DecapodError;
use rusqlite::Connection;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::Duration;
const MAX_RETRIES: u32 = 5;
const BASE_DELAY_MS: u64 = 100;
const MAX_DELAY_MS: u64 = 5_000;
const WRITE_BUSY_TIMEOUT_SECS: u32 = 5;
const READ_BUSY_TIMEOUT_SECS: u32 = 5;
struct PoolEntry {
write_lock: Mutex<()>,
db_path: PathBuf,
}
pub struct SqlitePool {
entries: Mutex<HashMap<PathBuf, &'static PoolEntry>>,
}
impl SqlitePool {
fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
fn get_entry(&self, db_path: &Path) -> Result<&'static PoolEntry, DecapodError> {
let canonical = db_path.to_path_buf();
let mut entries = self.entries.lock().map_err(|_| {
DecapodError::ValidationError("SqlitePool entries lock poisoned".to_string())
})?;
if let Some(entry) = entries.get(&canonical) {
return Ok(*entry);
}
let entry = Box::leak(Box::new(PoolEntry {
write_lock: Mutex::new(()),
db_path: canonical.clone(),
}));
entries.insert(canonical, entry);
Ok(entry)
}
pub fn with_write<F, R>(&self, db_path: &Path, f: F) -> Result<R, DecapodError>
where
F: FnOnce(&Connection) -> Result<R, DecapodError>,
{
let entry = self.get_entry(db_path)?;
let _guard = entry
.write_lock
.lock()
.map_err(|_| DecapodError::ValidationError("Pool write lock poisoned".to_string()))?;
let conn =
db::db_connect_pooled(&entry.db_path.to_string_lossy(), WRITE_BUSY_TIMEOUT_SECS)?;
f(&conn)
}
pub fn with_read<F, R>(&self, db_path: &Path, f: F) -> Result<R, DecapodError>
where
F: FnOnce(&Connection) -> Result<R, DecapodError>,
{
let conn = db::db_connect_pooled(&db_path.to_string_lossy(), READ_BUSY_TIMEOUT_SECS)?;
f(&conn)
}
}
#[allow(dead_code)]
fn retry_on_busy<F, R>(mut f: F) -> Result<R, DecapodError>
where
F: FnMut() -> Result<R, DecapodError>,
{
let mut attempt = 0u32;
loop {
match f() {
Ok(v) => return Ok(v),
Err(e) if is_busy_error(&e) && attempt < MAX_RETRIES => {
attempt += 1;
let delay_ms = (BASE_DELAY_MS * 2u64.pow(attempt - 1)).min(MAX_DELAY_MS);
thread::sleep(Duration::from_millis(delay_ms));
}
Err(e) => return Err(e),
}
}
}
fn is_busy_error(err: &DecapodError) -> bool {
match err {
DecapodError::RusqliteError(rusqlite::Error::SqliteFailure(code, _)) => matches!(
code.code,
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
),
_ => false,
}
}
pub fn global_pool() -> &'static SqlitePool {
static POOL: OnceLock<SqlitePool> = OnceLock::new();
POOL.get_or_init(SqlitePool::new)
}