Fix pool opening race and closing stutter

This commit is contained in:
Andras Schmelczer 2026-09-22 20:04:19 +01:00
commit 81b4a08a59
4 changed files with 90 additions and 37 deletions

View file

@ -1,5 +1,6 @@
pub mod cursors; pub mod cursors;
pub mod database; pub mod database;
pub mod weak_slots;
pub mod websocket; pub mod websocket;
use anyhow::Result; use anyhow::Result;

View file

@ -23,6 +23,7 @@ mod queries;
mod tests; mod tests;
use crate::{ use crate::{
app_state::weak_slots::get_or_create,
config::database_config::DatabaseConfig, config::database_config::DatabaseConfig,
utils::normalize_vault_id::{normalize_string, validate_vault_id}, utils::normalize_vault_id::{normalize_string, validate_vault_id},
}; };
@ -147,28 +148,44 @@ impl Database {
async fn get_connection_pool(&self, vault: &VaultId) -> Result<Pool<Sqlite>> { async fn get_connection_pool(&self, vault: &VaultId) -> Result<Pool<Sqlite>> {
let vault = normalize_string(vault); let vault = normalize_string(vault);
validate_vault_id(&vault)?; validate_vault_id(&vault)?;
let mut pools = self.connection_pools.lock().await;
if !pools.contains_key(&vault) { if let Some(pool) = self.existing_pool(&vault).await {
return Ok(pool);
}
let opening = get_or_create(self.opening_pools.lock(), vault.clone(), || {
Arc::new(Mutex::new(()))
})
.await;
let _opening = opening.lock().await;
if let Some(pool) = self.existing_pool(&vault).await {
return Ok(pool);
}
let file_name = Self::database_path(&self.config, &vault); let file_name = Self::database_path(&self.config, &vault);
let pool = Self::open_database(&self.config, &file_name).await?; let pool = Self::open_database(&self.config, &file_name).await?;
pools.insert(
vault.clone(), self.connection_pools.lock().await.insert(
vault,
PoolWithTimestamp { PoolWithTimestamp {
pool, pool: pool.clone(),
last_accessed: Instant::now(), last_accessed: Instant::now(),
}, },
); );
Ok(pool)
} }
let pool_with_timestamp = pools async fn existing_pool(&self, vault: &VaultId) -> Option<Pool<Sqlite>> {
.get_mut(&vault) self.connection_pools
.expect("Pool was just inserted or already exists"); .lock()
.await
// Update last accessed time .get_mut(vault)
pool_with_timestamp.last_accessed = Instant::now(); .map(|entry| {
entry.last_accessed = Instant::now();
Ok(pool_with_timestamp.pool.clone()) entry.pool.clone()
})
} }
/// Attempting to write from this transaction might result in a /// Attempting to write from this transaction might result in a
@ -203,16 +220,25 @@ impl Database {
.iter() .iter()
.filter(|(_, pool_with_timestamp)| { .filter(|(_, pool_with_timestamp)| {
now.duration_since(pool_with_timestamp.last_accessed) > idle_timeout now.duration_since(pool_with_timestamp.last_accessed) > idle_timeout
&& pool_with_timestamp.pool.num_idle()
== pool_with_timestamp.pool.size() as usize
}) })
.map(|(vault_id, _)| vault_id.clone()) .map(|(vault_id, _)| vault_id.clone())
.collect(); .collect();
// Close and remove idle pools let closing: Vec<_> = vaults_to_remove
for vault_id in &vaults_to_remove { .into_iter()
if let Some(pool_with_timestamp) = pools.remove(vault_id) { .filter_map(|vault| pools.remove(&vault).map(|entry| (vault, entry.pool)))
info!("Closing idle database connection pool for vault `{vault_id}`"); .collect();
pool_with_timestamp.pool.close().await;
} // Don't hold the pools mutex while closing the removed vaults
drop(pools);
// A checkout racing cleanup can delay close. It must never retain the
// mutex used to access every other vault in the process.
for (vault, pool) in closing {
info!("Closing idle database connection pool for vault `{vault}`");
pool.close().await;
} }
} }

View file

@ -0,0 +1,26 @@
use std::{
collections::HashMap,
future::Future,
hash::Hash,
ops::DerefMut,
sync::{Arc, Weak},
};
/// Get-or-create an `Arc<T>` for `key` in a weak map behind a mutex
pub async fn get_or_create<K, T, G, L>(lock: L, key: K, create: impl FnOnce() -> Arc<T>) -> Arc<T>
where
K: Clone + Eq + Hash,
G: DerefMut<Target = HashMap<K, Weak<T>>>,
L: Future<Output = G>,
{
let mut map = lock.await;
map.retain(|_, value| value.strong_count() > 0);
if let Some(value) = map.get(&key).and_then(Weak::upgrade) {
value
} else {
let value = create();
map.insert(key, Arc::downgrade(&value));
value
}
}

View file

@ -1,5 +1,11 @@
use super::models::CursorPositionFromServer; use super::models::CursorPositionFromServer;
use crate::{app_state::database::models::VaultId, config::server_config::ServerConfig}; use crate::{
app_state::{
database::models::VaultId,
weak_slots::get_or_create,
},
config::server_config::ServerConfig,
};
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tokio::sync::{Mutex, broadcast}; use tokio::sync::{Mutex, broadcast};
@ -25,19 +31,13 @@ impl Broadcasts {
} }
} }
pub fn try_admit(&self, vault: &VaultId) -> Option<tokio::sync::OwnedSemaphorePermit> { pub async fn try_admit(&self, vault: &VaultId) -> Option<tokio::sync::OwnedSemaphorePermit> {
let mut limits = self.admission.lock().unwrap(); let limit = get_or_create(
async { self.admission.lock().unwrap() },
limits.retain(|_, limit| limit.strong_count() > 0); vault.clone(),
|| Arc::new(tokio::sync::Semaphore::new(self.capacity)),
let limit = limits )
.get(vault) .await;
.and_then(std::sync::Weak::upgrade)
.unwrap_or_else(|| {
let limit = Arc::new(tokio::sync::Semaphore::new(self.capacity));
limits.insert(vault.clone(), Arc::downgrade(&limit));
limit
});
limit.try_acquire_owned().ok() limit.try_acquire_owned().ok()
} }