simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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:
MCargo.lock | 140+++----------------------------------------------------------------------------
MCargo.toml | 2--
Msrc/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 }) } // ======================================================================