use std::time::{Duration, Instant};
use async_trait::async_trait;
use uuid::Uuid;
use crate::advisory_key::AdvisoryKey;
use crate::error::{PersistenceError, PersistenceResult};
#[async_trait]
pub trait DistributedLock: Send + Sync + 'static {
async fn try_acquire(
&self,
key: &AdvisoryKey,
ttl: Duration,
) -> PersistenceResult<Option<LockGuard>>;
async fn acquire(
&self,
key: &AdvisoryKey,
ttl: Duration,
deadline: Duration,
) -> PersistenceResult<LockGuard>;
async fn extend(&self, guard: &LockGuard, ttl: Duration) -> PersistenceResult<bool>;
async fn release(&self, guard: LockGuard) -> PersistenceResult<()>;
}
#[derive(Debug)]
pub struct LockGuard {
key: AdvisoryKey,
token: String,
acquired_at: Instant,
released: bool,
}
impl LockGuard {
pub fn new(key: AdvisoryKey) -> Self {
Self {
key,
token: Uuid::new_v4().to_string(),
acquired_at: Instant::now(),
released: false,
}
}
pub const fn key(&self) -> &AdvisoryKey {
&self.key
}
pub fn token(&self) -> &str {
&self.token
}
pub fn held_for(&self) -> Duration {
self.acquired_at.elapsed()
}
pub fn mark_released(&mut self) {
self.released = true;
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
if !self.released {
tracing::warn!(
target: "entelix_persistence::lock",
key = %self.key,
held_ms = self.acquired_at.elapsed().as_millis() as u64,
"LockGuard dropped without explicit release; lock will expire by TTL only"
);
}
}
}
pub const DEFAULT_SESSION_LOCK_TTL: Duration = Duration::from_secs(30);
pub const DEFAULT_SESSION_LOCK_DEADLINE: Duration = Duration::from_secs(5);
pub async fn with_session_lock<L, F, Fut, T, E>(
lock: &L,
tenant_id: &entelix_core::TenantId,
thread_id: &str,
ttl: Option<Duration>,
deadline: Option<Duration>,
f: F,
) -> PersistenceResult<T>
where
L: DistributedLock + ?Sized,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = std::result::Result<T, E>>,
E: Into<PersistenceError>,
{
let key = AdvisoryKey::for_session(tenant_id, thread_id);
let ttl = ttl.unwrap_or(DEFAULT_SESSION_LOCK_TTL);
let deadline = deadline.unwrap_or(DEFAULT_SESSION_LOCK_DEADLINE);
let guard = lock.acquire(&key, ttl, deadline).await?;
let outcome = f().await;
if let Err(e) = lock.release(guard).await {
tracing::warn!(
target: "entelix_persistence::lock",
error = %e,
"lock release failed; relying on TTL expiry"
);
}
outcome.map_err(Into::into)
}