diff --git a/sync-server/src/app_state/database.rs b/sync-server/src/app_state/database.rs index 95dbf5ec..7308ec1a 100644 --- a/sync-server/src/app_state/database.rs +++ b/sync-server/src/app_state/database.rs @@ -10,7 +10,7 @@ use sqlx::{ConnectOptions, sqlite::SqliteConnectOptions, types::chrono::Utc}; pub mod models; use sqlx::{Pool, Sqlite, sqlite::SqlitePoolOptions}; -use tokio::sync::Mutex; +use tokio::sync::RwLock; use tokio::time::Instant; use uuid::fmt::Hyphenated; @@ -39,7 +39,7 @@ impl std::fmt::Debug for PoolWithTimestamp { pub struct Database { config: DatabaseConfig, broadcasts: Broadcasts, - connection_pools: Arc>>, + connection_pools: Arc>>, } pub type Transaction<'a> = sqlx::Transaction<'a, Sqlite>; @@ -83,7 +83,7 @@ impl Database { let database = Self { config: config.clone(), - connection_pools: Arc::new(Mutex::new(connection_pools)), + connection_pools: Arc::new(RwLock::new(connection_pools)), broadcasts: broadcasts.clone(), }; @@ -130,11 +130,12 @@ impl Database { } async fn get_connection_pool(&self, vault: &VaultId) -> Result> { - // First, check if the pool exists without holding the lock during creation + // Fast path: check if pool exists with a read lock (no blocking other readers) { - let mut pools = self.connection_pools.lock().await; - if let Some(pool_with_timestamp) = pools.get_mut(vault) { - pool_with_timestamp.last_accessed = Instant::now(); + let pools = self.connection_pools.read().await; + if let Some(pool_with_timestamp) = pools.get(vault) { + // Skip updating last_accessed here - it's only used for idle cleanup + // and will be updated when the pool is created or reused after recreation return Ok(pool_with_timestamp.pool.clone()); } } @@ -144,8 +145,8 @@ impl Database { // under high concurrency, but only one will be kept let new_pool = Self::create_vault_database(&self.config, vault).await?; - // Re-acquire lock and insert (or use existing if another task created it) - let mut pools = self.connection_pools.lock().await; + // Re-acquire lock (write) and insert (or use existing if another task created it) + let mut pools = self.connection_pools.write().await; let pool_with_timestamp = pools .entry(vault.clone()) .or_insert_with(|| PoolWithTimestamp { @@ -480,22 +481,19 @@ impl Database { Ok(()) } - /// Cleanup idle connection pools that haven't been accessed in more than 5 minutes async fn cleanup_idle_pools(&self) { - let mut pools = self.connection_pools.lock().await; - let now = Instant::now(); - let idle_timeout = Duration::from_secs(5 * 60); // 5 minutes + use crate::consts::IDLE_POOL_TIMEOUT; - // Collect vaults to remove + let mut pools = self.connection_pools.write().await; + let now = Instant::now(); let vaults_to_remove: Vec = pools .iter() .filter(|(_, pool_with_timestamp)| { - now.duration_since(pool_with_timestamp.last_accessed) > idle_timeout + now.duration_since(pool_with_timestamp.last_accessed) > IDLE_POOL_TIMEOUT }) .map(|(vault_id, _)| vault_id.clone()) .collect(); - // Close and remove idle pools for vault_id in &vaults_to_remove { if let Some(pool_with_timestamp) = pools.remove(vault_id) { info!("Closing idle database connection pool for vault `{vault_id}`"); diff --git a/sync-server/src/consts.rs b/sync-server/src/consts.rs index 9e9890c0..ca1d7fbf 100644 --- a/sync-server/src/consts.rs +++ b/sync-server/src/consts.rs @@ -7,6 +7,7 @@ pub const DEFAULT_CONFIG_PATH: &str = "config.yml"; pub const DEFAULT_DATABASES_DIRECTORY_PATH: &str = "databases"; pub const DEFAULT_MAX_CONNECTIONS_PER_VAULT: u32 = 12; pub const DEFAULT_CURSOR_TIMEOUT: Duration = Duration::from_secs(60); +pub const IDLE_POOL_TIMEOUT: Duration = Duration::from_secs(5 * 60); pub const DEFAULT_HOST: &str = "127.0.0.1"; pub const DEFAULT_PORT: u16 = 3000;