Fix pool opening race and closing stutter
This commit is contained in:
parent
4324792f54
commit
81b4a08a59
4 changed files with 90 additions and 37 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
let file_name = Self::database_path(&self.config, &vault);
|
return Ok(pool);
|
||||||
let pool = Self::open_database(&self.config, &file_name).await?;
|
|
||||||
pools.insert(
|
|
||||||
vault.clone(),
|
|
||||||
PoolWithTimestamp {
|
|
||||||
pool,
|
|
||||||
last_accessed: Instant::now(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool_with_timestamp = pools
|
let opening = get_or_create(self.opening_pools.lock(), vault.clone(), || {
|
||||||
.get_mut(&vault)
|
Arc::new(Mutex::new(()))
|
||||||
.expect("Pool was just inserted or already exists");
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
// Update last accessed time
|
let _opening = opening.lock().await;
|
||||||
pool_with_timestamp.last_accessed = Instant::now();
|
|
||||||
|
|
||||||
Ok(pool_with_timestamp.pool.clone())
|
if let Some(pool) = self.existing_pool(&vault).await {
|
||||||
|
return Ok(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = Self::database_path(&self.config, &vault);
|
||||||
|
let pool = Self::open_database(&self.config, &file_name).await?;
|
||||||
|
|
||||||
|
self.connection_pools.lock().await.insert(
|
||||||
|
vault,
|
||||||
|
PoolWithTimestamp {
|
||||||
|
pool: pool.clone(),
|
||||||
|
last_accessed: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn existing_pool(&self, vault: &VaultId) -> Option<Pool<Sqlite>> {
|
||||||
|
self.connection_pools
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_mut(vault)
|
||||||
|
.map(|entry| {
|
||||||
|
entry.last_accessed = Instant::now();
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
26
sync-server/src/app_state/weak_slots.rs
Normal file
26
sync-server/src/app_state/weak_slots.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue