Fix startup failure from fd exhaustion when launched via Finder (#500)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-07-05 08:49:23 -07:00
committed by GitHub
parent f2972ee534
commit b332a0eba9
6 changed files with 46 additions and 35 deletions
Generated
+12 -2
View File
@@ -3885,9 +3885,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "libdbus-sys" name = "libdbus-sys"
@@ -6649,6 +6649,15 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "rolldown" name = "rolldown"
version = "0.1.0" version = "0.1.0"
@@ -10932,6 +10941,7 @@ dependencies = [
"r2d2_sqlite", "r2d2_sqlite",
"rand 0.9.1", "rand 0.9.1",
"reqwest 0.12.20", "reqwest 0.12.20",
"rlimit",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
+3
View File
@@ -24,6 +24,9 @@ tauri-build = { version = "2.6.1", features = [] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work 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] [dependencies]
charset = "0.1.5" charset = "0.1.5"
chrono = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] }
+8
View File
@@ -1676,6 +1676,14 @@ async fn cmd_check_for_updates<R: Runtime>(
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
#[cfg_attr(feature = "cef", tauri::cef_entry_point)] #[cfg_attr(feature = "cef", tauri::cef_entry_point)]
pub fn run() { 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::<TauriRuntime>::default().plugin( let mut builder = tauri::Builder::<TauriRuntime>::default().plugin(
Builder::default() Builder::default()
.targets([ .targets([
+6 -9
View File
@@ -5,7 +5,6 @@ use log::{debug, info};
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, params}; use rusqlite::{OptionalExtension, params};
use std::sync::{Arc, Mutex};
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations"); static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations");
@@ -25,23 +24,21 @@ impl BodyChunk {
} }
/// Manages the blob database connection pool. /// 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)] #[derive(Debug, Clone)]
pub struct BlobManager { pub struct BlobManager {
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>, pool: Pool<SqliteConnectionManager>,
} }
impl BlobManager { impl BlobManager {
pub fn new(pool: Pool<SqliteConnectionManager>) -> Self { pub fn new(pool: Pool<SqliteConnectionManager>) -> Self {
Self { pool: Arc::new(Mutex::new(pool)) } Self { pool }
} }
pub fn connect(&self) -> BlobContext { pub fn connect(&self) -> BlobContext {
let conn = self let conn = self.pool.get().expect("Failed to get blob DB connection from pool");
.pool
.lock()
.expect("Failed to gain lock on blob DB")
.get()
.expect("Failed to get blob DB connection from pool");
BlobContext { conn } BlobContext { conn }
} }
} }
+8 -3
View File
@@ -54,11 +54,15 @@ pub fn init_standalone(
create_dir_all(parent)?; 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:?}"); info!("Initializing app database {db_path:?}");
let manager = sqlite_file_manager(db_path); let manager = sqlite_file_manager(db_path);
let pool = Pool::builder() let pool = Pool::builder()
.max_size(100) .max_size(20)
.min_idle(Some(2))
.connection_timeout(Duration::from_secs(10)) .connection_timeout(Duration::from_secs(10))
.build(manager) .build(manager)
.map_err(|e| Error::Database(e.to_string()))?; .map_err(|e| Error::Database(e.to_string()))?;
@@ -70,7 +74,8 @@ pub fn init_standalone(
// Blob database pool // Blob database pool
let blob_manager = sqlite_file_manager(blob_path); let blob_manager = sqlite_file_manager(blob_path);
let blob_pool = Pool::builder() let blob_pool = Pool::builder()
.max_size(50) .max_size(10)
.min_idle(Some(1))
.connection_timeout(Duration::from_secs(10)) .connection_timeout(Duration::from_secs(10))
.build(blob_manager) .build(blob_manager)
.map_err(|e| Error::Database(e.to_string()))?; .map_err(|e| Error::Database(e.to_string()))?;
+9 -21
View File
@@ -4,27 +4,25 @@ use crate::util::ModelPayload;
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::TransactionBehavior; use rusqlite::TransactionBehavior;
use std::sync::{Arc, Mutex, mpsc}; use std::sync::mpsc;
use yaak_database::{ConnectionOrTx, DbContext}; 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)] #[derive(Debug, Clone)]
pub struct QueryManager { pub struct QueryManager {
pool: Arc<Mutex<Pool<SqliteConnectionManager>>>, pool: Pool<SqliteConnectionManager>,
events_tx: mpsc::Sender<ModelPayload>, events_tx: mpsc::Sender<ModelPayload>,
} }
impl QueryManager { impl QueryManager {
pub fn new(pool: Pool<SqliteConnectionManager>, events_tx: mpsc::Sender<ModelPayload>) -> Self { pub fn new(pool: Pool<SqliteConnectionManager>, events_tx: mpsc::Sender<ModelPayload>) -> Self {
QueryManager { pool: Arc::new(Mutex::new(pool)), events_tx } QueryManager { pool, events_tx }
} }
pub fn connect(&self) -> ClientDb<'_> { pub fn connect(&self) -> ClientDb<'_> {
let conn = self let conn = self.pool.get().expect("Failed to get a new DB connection from the pool");
.pool
.lock()
.expect("Failed to gain lock on DB")
.get()
.expect("Failed to get a new DB connection from the pool");
let ctx = DbContext::new(ConnectionOrTx::Connection(conn)); let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
ClientDb::new(ctx, self.events_tx.clone()) ClientDb::new(ctx, self.events_tx.clone())
} }
@@ -33,12 +31,7 @@ impl QueryManager {
where where
F: FnOnce(&ClientDb) -> T, F: FnOnce(&ClientDb) -> T,
{ {
let conn = self let conn = self.pool.get().expect("Failed to get new DB connection from the pool");
.pool
.lock()
.expect("Failed to gain lock on DB for transaction")
.get()
.expect("Failed to get new DB connection from the pool");
let ctx = DbContext::new(ConnectionOrTx::Connection(conn)); let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
let db = ClientDb::new(ctx, self.events_tx.clone()); let db = ClientDb::new(ctx, self.events_tx.clone());
@@ -53,12 +46,7 @@ impl QueryManager {
where where
E: From<crate::error::Error>, E: From<crate::error::Error>,
{ {
let mut conn = self let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool");
.pool
.lock()
.expect("Failed to gain lock on DB for transaction")
.get()
.expect("Failed to get new DB connection from the pool");
let tx = conn let tx = conn
.transaction_with_behavior(TransactionBehavior::Immediate) .transaction_with_behavior(TransactionBehavior::Immediate)
.expect("Failed to start DB transaction"); .expect("Failed to start DB transaction");