commit db0f6ea16c551f15d6f3d103f77793648f1a1d3f
parent ee0630ef97fbdb420e046c67853e7581048b389e
Author: Silas Brack <silasbrack@gmail.com>
Date: Fri, 12 Jun 2026 22:24:24 +0200
perf: replace r2d2 with hand-rolled connection pool
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
| M | Cargo.lock | | | 140 | +++---------------------------------------------------------------------------- |
| M | Cargo.toml | | | 2 | -- |
| M | src/database.rs | | | 110 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
3 files changed, 80 insertions(+), 172 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -37,7 +37,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
- "cpufeatures 0.2.17",
+ "cpufeatures",
"password-hash",
]
@@ -246,17 +246,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "chacha20"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "rand_core 0.10.1",
-]
-
-[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -307,15 +296,6 @@ dependencies = [
]
[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -521,7 +501,6 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
- "rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -906,15 +885,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1085,36 +1055,13 @@ dependencies = [
]
[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
- "rand_core 0.6.4",
+ "rand_core",
"subtle",
]
@@ -1211,28 +1158,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
-name = "r2d2"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
-dependencies = [
- "log",
- "parking_lot",
- "scheduled-thread-pool",
-]
-
-[[package]]
-name = "r2d2_sqlite"
-version = "0.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a982edf65c129796dba72f8775b292ef482b40d035e827a9825b3bc07ccc5f2"
-dependencies = [
- "r2d2",
- "rusqlite",
- "uuid",
-]
-
-[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1240,18 +1165,7 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
-dependencies = [
- "chacha20",
- "getrandom 0.4.2",
- "rand_core 0.10.1",
+ "rand_core",
]
[[package]]
@@ -1261,7 +1175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core 0.6.4",
+ "rand_core",
]
[[package]]
@@ -1274,21 +1188,6 @@ dependencies = [
]
[[package]]
-name = "rand_core"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1351,21 +1250,6 @@ dependencies = [
]
[[package]]
-name = "scheduled-thread-pool"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
-dependencies = [
- "parking_lot",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1477,9 +1361,7 @@ dependencies = [
"chrono",
"jsonwebtoken",
"lettre",
- "r2d2",
- "r2d2_sqlite",
- "rand 0.8.6",
+ "rand",
"rusqlite",
"serde",
"tokio",
@@ -1794,18 +1676,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
-name = "uuid"
-version = "1.23.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
-dependencies = [
- "getrandom 0.4.2",
- "js-sys",
- "rand 0.10.1",
- "wasm-bindgen",
-]
-
-[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -13,8 +13,6 @@ serde = { version = "1", features = ["derive"] }
tokio-stream = { version = "0.1", features = ["sync"] }
tracing = "0.1"
rusqlite = "0.31"
-r2d2 = "0.8"
-r2d2_sqlite = "0.24"
argon2 = "0.5"
jsonwebtoken = "9"
rand = "0.8"
diff --git a/src/database.rs b/src/database.rs
@@ -1,8 +1,7 @@
use std::collections::HashMap;
+use std::sync::Mutex;
-use r2d2::Pool;
-use r2d2_sqlite::SqliteConnectionManager;
-use rusqlite::params;
+use rusqlite::{params, Connection};
use crate::error::AppError;
@@ -105,47 +104,88 @@ pub struct StoryMeta {
// Database
// ==========================================================================
+const POOL_SIZE: usize = 10;
+const PRAGMAS: &str = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA cache_size=-64000;";
+
pub struct Database {
- pool: Pool<SqliteConnectionManager>,
+ path: String,
+ conns: Mutex<Vec<Connection>>,
+}
+
+/// RAII guard that returns a connection to the pool on drop.
+pub struct PooledConn<'a> {
+ conn: Option<Connection>,
+ pool: &'a Database,
+}
+
+impl<'a> std::ops::Deref for PooledConn<'a> {
+ type Target = Connection;
+ fn deref(&self) -> &Connection {
+ self.conn.as_ref().unwrap()
+ }
+}
+
+impl<'a> Drop for PooledConn<'a> {
+ fn drop(&mut self) {
+ if let Some(conn) = self.conn.take() {
+ let mut conns = self.pool.conns.lock().unwrap();
+ if conns.len() < POOL_SIZE {
+ conns.push(conn);
+ }
+ }
+ }
}
impl Database {
pub fn new(path: &str) -> Result<Self, AppError> {
- // Ensure parent directory exists
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(parent)
- .map_err(|e| AppError::Database(format!("Failed to create db directory: {}", e)))?;
+ .map_err(|e| AppError::Database(format!("Failed to create db directory: {e}")))?;
}
- // Run migrations on a direct connection first
- let migration_conn = rusqlite::Connection::open(path)
- .map_err(|e| AppError::Database(format!("Failed to open db for migrations: {}", e)))?;
- migration_conn.execute_batch(
- "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA cache_size=-64000;"
- ).map_err(|e| AppError::Database(format!("Failed to set PRAGMAs: {}", e)))?;
- crate::migrations::run(&migration_conn)
- .map_err(|e| AppError::Database(e))?;
- drop(migration_conn);
-
- // Set up pool with per-connection PRAGMA init
- let manager = SqliteConnectionManager::file(path)
- .with_init(|c| {
- c.execute_batch(
- "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA cache_size=-64000;"
- )
- });
- let pool = Pool::builder()
- .max_size(10)
- .build(manager)
- .map_err(|e| AppError::Database(format!("Failed to create pool: {}", e)))?;
-
- Ok(Self { pool })
- }
-
- fn get_conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>, AppError> {
- self.pool
- .get()
- .map_err(|e| AppError::Database(format!("Failed to get connection: {}", e)))
+ // Run migrations
+ let conn = Connection::open(path)
+ .map_err(|e| AppError::Database(format!("Failed to open db: {e}")))?;
+ conn.execute_batch(PRAGMAS)
+ .map_err(|e| AppError::Database(format!("Failed to set PRAGMAs: {e}")))?;
+ crate::migrations::run(&conn).map_err(AppError::Database)?;
+
+ // Pre-fill pool
+ let mut conns = Vec::with_capacity(POOL_SIZE);
+ conns.push(conn);
+ for _ in 1..POOL_SIZE {
+ let c = Connection::open(path)
+ .map_err(|e| AppError::Database(format!("Failed to open db: {e}")))?;
+ c.execute_batch(PRAGMAS)
+ .map_err(|e| AppError::Database(format!("Failed to set PRAGMAs: {e}")))?;
+ conns.push(c);
+ }
+
+ Ok(Self {
+ path: path.to_string(),
+ conns: Mutex::new(conns),
+ })
+ }
+
+ fn get_conn(&self) -> Result<PooledConn<'_>, AppError> {
+ let conn = {
+ let mut conns = self.conns.lock().unwrap();
+ conns.pop()
+ };
+
+ let conn = match conn {
+ Some(c) => c,
+ None => {
+ // Pool exhausted, open a new connection
+ let c = Connection::open(&self.path)
+ .map_err(|e| AppError::Database(format!("Failed to open db: {e}")))?;
+ c.execute_batch(PRAGMAS)
+ .map_err(|e| AppError::Database(format!("Failed to set PRAGMAs: {e}")))?;
+ c
+ }
+ };
+
+ Ok(PooledConn { conn: Some(conn), pool: self })
}
// ======================================================================