diff --git a/Cargo.lock b/Cargo.lock index 9b28c4c2..6315ae03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3885,9 +3885,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -6649,6 +6649,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rlimit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" +dependencies = [ + "libc", +] + [[package]] name = "rolldown" version = "0.1.0" @@ -10932,6 +10941,7 @@ dependencies = [ "r2d2_sqlite", "rand 0.9.1", "reqwest 0.12.20", + "rlimit", "serde", "serde_json", "tauri", diff --git a/crates-tauri/yaak-app-client/Cargo.toml b/crates-tauri/yaak-app-client/Cargo.toml index 4bdb1c8c..ee20039f 100644 --- a/crates-tauri/yaak-app-client/Cargo.toml +++ b/crates-tauri/yaak-app-client/Cargo.toml @@ -24,6 +24,9 @@ tauri-build = { version = "2.6.1", features = [] } [target.'cfg(target_os = "linux")'.dependencies] openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +rlimit = "0.11" # Raise the launchd 256 open-file soft limit at startup + [dependencies] charset = "0.1.5" chrono = { workspace = true, features = ["serde"] } diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index ea43bced..696ecc2a 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -1676,6 +1676,14 @@ async fn cmd_check_for_updates( #[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(feature = "cef", tauri::cef_entry_point)] pub fn run() { + // GUI apps launched via Finder/launchd inherit a 256 open-file soft limit on macOS + // (1024 on most Linux desktops). SQLite WAL connections hold ~3 fds each, so raise + // the limit toward the hard cap before opening any DB pools. + #[cfg(any(target_os = "macos", target_os = "linux"))] + if let Err(e) = rlimit::increase_nofile_limit(10240) { + eprintln!("Failed to raise open-file limit: {e}"); + } + let mut builder = tauri::Builder::::default().plugin( Builder::default() .targets([ diff --git a/crates/yaak-models/src/blob_manager.rs b/crates/yaak-models/src/blob_manager.rs index 2f2fc2ef..31d4b155 100644 --- a/crates/yaak-models/src/blob_manager.rs +++ b/crates/yaak-models/src/blob_manager.rs @@ -5,7 +5,6 @@ use log::{debug, info}; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{OptionalExtension, params}; -use std::sync::{Arc, Mutex}; static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations"); @@ -25,23 +24,21 @@ impl BodyChunk { } /// Manages the blob database connection pool. +// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the +// blocking `get()` serializes every blob access behind the slowest waiter, freezing the +// whole app whenever the pool is exhausted. #[derive(Debug, Clone)] pub struct BlobManager { - pool: Arc>>, + pool: Pool, } impl BlobManager { pub fn new(pool: Pool) -> Self { - Self { pool: Arc::new(Mutex::new(pool)) } + Self { pool } } pub fn connect(&self) -> BlobContext { - let conn = self - .pool - .lock() - .expect("Failed to gain lock on blob DB") - .get() - .expect("Failed to get blob DB connection from pool"); + let conn = self.pool.get().expect("Failed to get blob DB connection from pool"); BlobContext { conn } } } diff --git a/crates/yaak-models/src/lib.rs b/crates/yaak-models/src/lib.rs index d5a546b4..a642eaee 100644 --- a/crates/yaak-models/src/lib.rs +++ b/crates/yaak-models/src/lib.rs @@ -54,11 +54,15 @@ pub fn init_standalone( create_dir_all(parent)?; } - // Main database pool + // Main database pool. Sized for concurrent in-flight queries, not concurrent app + // features — connections are held per-statement, so even heavy fan-out (e.g. many + // gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections + // hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit. info!("Initializing app database {db_path:?}"); let manager = sqlite_file_manager(db_path); let pool = Pool::builder() - .max_size(100) + .max_size(20) + .min_idle(Some(2)) .connection_timeout(Duration::from_secs(10)) .build(manager) .map_err(|e| Error::Database(e.to_string()))?; @@ -70,7 +74,8 @@ pub fn init_standalone( // Blob database pool let blob_manager = sqlite_file_manager(blob_path); let blob_pool = Pool::builder() - .max_size(50) + .max_size(10) + .min_idle(Some(1)) .connection_timeout(Duration::from_secs(10)) .build(blob_manager) .map_err(|e| Error::Database(e.to_string()))?; diff --git a/crates/yaak-models/src/query_manager.rs b/crates/yaak-models/src/query_manager.rs index ebd7f87d..b570f153 100644 --- a/crates/yaak-models/src/query_manager.rs +++ b/crates/yaak-models/src/query_manager.rs @@ -4,27 +4,25 @@ use crate::util::ModelPayload; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::TransactionBehavior; -use std::sync::{Arc, Mutex, mpsc}; +use std::sync::mpsc; use yaak_database::{ConnectionOrTx, DbContext}; +// Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the +// blocking `get()` serializes every DB access behind the slowest waiter, freezing the +// whole app whenever the pool is exhausted. #[derive(Debug, Clone)] pub struct QueryManager { - pool: Arc>>, + pool: Pool, events_tx: mpsc::Sender, } impl QueryManager { pub fn new(pool: Pool, events_tx: mpsc::Sender) -> Self { - QueryManager { pool: Arc::new(Mutex::new(pool)), events_tx } + QueryManager { pool, events_tx } } pub fn connect(&self) -> ClientDb<'_> { - let conn = self - .pool - .lock() - .expect("Failed to gain lock on DB") - .get() - .expect("Failed to get a new DB connection from the pool"); + let conn = self.pool.get().expect("Failed to get a new DB connection from the pool"); let ctx = DbContext::new(ConnectionOrTx::Connection(conn)); ClientDb::new(ctx, self.events_tx.clone()) } @@ -33,12 +31,7 @@ impl QueryManager { where F: FnOnce(&ClientDb) -> T, { - let conn = self - .pool - .lock() - .expect("Failed to gain lock on DB for transaction") - .get() - .expect("Failed to get new DB connection from the pool"); + let conn = self.pool.get().expect("Failed to get new DB connection from the pool"); let ctx = DbContext::new(ConnectionOrTx::Connection(conn)); let db = ClientDb::new(ctx, self.events_tx.clone()); @@ -53,12 +46,7 @@ impl QueryManager { where E: From, { - let mut conn = self - .pool - .lock() - .expect("Failed to gain lock on DB for transaction") - .get() - .expect("Failed to get new DB connection from the pool"); + let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool"); let tx = conn .transaction_with_behavior(TransactionBehavior::Immediate) .expect("Failed to start DB transaction");