use async_trait::async_trait;
use crate::core::StoreError;
#[derive(Debug)]
pub struct Lease {
scope: String,
token: u64,
}
impl Lease {
#[must_use]
pub fn scope(&self) -> &str {
&self.scope
}
}
#[async_trait]
pub trait ErasureCoordinator: Send + Sync + std::fmt::Debug {
async fn acquire(&self, scope: &str) -> Result<Lease, StoreError>;
async fn release(&self, lease: Lease) -> Result<(), StoreError>;
fn is_distributed(&self) -> bool;
}
pub async fn under_lock<T, F, Fut>(
coordinator: &dyn ErasureCoordinator,
scope: &str,
work: F,
) -> Result<T, StoreError>
where
F: FnOnce() -> Fut + Send,
Fut: std::future::Future<Output = Result<T, StoreError>> + Send,
{
let lease = coordinator.acquire(scope).await?;
let outcome = work().await;
let released = coordinator.release(lease).await;
match (outcome, released) {
(Err(work), _) => Err(work),
(Ok(value), Ok(())) => Ok(value),
(Ok(_), Err(release)) => Err(release),
}
}
#[derive(Debug, Default)]
pub struct LocalCoordinator {
scopes:
std::sync::Mutex<std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<()>>>>,
held: std::sync::Mutex<std::collections::HashMap<u64, tokio::sync::OwnedMutexGuard<()>>>,
next: std::sync::atomic::AtomicU64,
}
impl LocalCoordinator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl ErasureCoordinator for LocalCoordinator {
async fn acquire(&self, scope: &str) -> Result<Lease, StoreError> {
let lock = {
let mut scopes = self.scopes.lock().expect("lifecycle scopes");
std::sync::Arc::clone(
scopes
.entry(scope.to_owned())
.or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(()))),
)
};
let guard = lock.lock_owned().await;
let token = self.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.held
.lock()
.expect("lifecycle leases")
.insert(token, guard);
Ok(Lease {
scope: scope.to_owned(),
token,
})
}
async fn release(&self, lease: Lease) -> Result<(), StoreError> {
self.held
.lock()
.expect("lifecycle leases")
.remove(&lease.token);
Ok(())
}
fn is_distributed(&self) -> bool {
false
}
}
#[cfg(feature = "postgres")]
#[derive(Debug)]
pub struct PostgresCoordinator {
pool: deadpool_postgres::Pool,
held: tokio::sync::Mutex<std::collections::HashMap<u64, deadpool_postgres::Object>>,
next: std::sync::atomic::AtomicU64,
}
#[cfg(feature = "postgres")]
impl PostgresCoordinator {
#[must_use]
pub fn new(pool: deadpool_postgres::Pool) -> Self {
Self {
pool,
held: tokio::sync::Mutex::new(std::collections::HashMap::new()),
next: std::sync::atomic::AtomicU64::new(0),
}
}
pub(crate) fn scope_key(scope: &str) -> i64 {
Self::key(scope)
}
fn key(scope: &str) -> i64 {
use sha2::{Digest as _, Sha256};
let digest = Sha256::digest(scope.as_bytes());
i64::from_be_bytes(digest[..8].try_into().expect("8 bytes"))
}
}
#[cfg(feature = "postgres")]
#[async_trait]
impl ErasureCoordinator for PostgresCoordinator {
async fn acquire(&self, scope: &str) -> Result<Lease, StoreError> {
let client = self
.pool
.get()
.await
.map_err(|e| StoreError::Backend(e.to_string()))?;
client
.execute("SELECT pg_advisory_lock($1)", &[&Self::key(scope)])
.await
.map_err(|e| StoreError::Backend(e.to_string()))?;
let token = self.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.held.lock().await.insert(token, client);
Ok(Lease {
scope: scope.to_owned(),
token,
})
}
async fn release(&self, lease: Lease) -> Result<(), StoreError> {
let Some(client) = self.held.lock().await.remove(&lease.token) else {
return Ok(());
};
let unlocked = client
.execute(
"SELECT pg_advisory_unlock($1)",
&[&Self::key(lease.scope())],
)
.await;
drop(client);
unlocked
.map(|_| ())
.map_err(|e| StoreError::Backend(e.to_string()))
}
fn is_distributed(&self) -> bool {
true
}
}