ppoppo-infra 0.1.0

Backend-agnostic infrastructure traits for caching, queuing, and messaging
Documentation
//! Backend-agnostic distributed lock trait.

use std::sync::Arc;

use async_trait::async_trait;

use crate::Result;

/// Distributed lock operations.
///
/// Session-level locking with backend-agnostic RAII via [`LockGuard`].
/// Transaction-level locks remain backend-specific.
#[async_trait]
pub trait Lock: Send + Sync {
    /// Acquire a lock (blocking). Blocks until the lock is acquired.
    async fn acquire(&self, namespace: &str, resource_id: &str) -> Result<()>;

    /// Try to acquire a lock (non-blocking).
    ///
    /// Returns `true` if lock acquired, `false` if already held.
    async fn try_acquire(&self, namespace: &str, resource_id: &str) -> Result<bool>;

    /// Release a lock. Returns `true` if lock was held and released.
    async fn release(&self, namespace: &str, resource_id: &str) -> Result<bool>;

    /// Acquire a lock with timeout.
    ///
    /// Retries acquiring the lock until timeout is reached.
    async fn acquire_with_timeout(
        &self,
        namespace: &str,
        resource_id: &str,
        timeout_ms: i32,
        retry_interval_ms: u64,
    ) -> Result<()>;
}

/// Backend-agnostic RAII lock guard.
///
/// Wraps `Arc<dyn Lock>` and releases via the trait's `release()` method.
/// On drop without explicit release, spawns a tokio task for async cleanup.
pub struct LockGuard {
    lock: Arc<dyn Lock>,
    namespace: String,
    resource_id: String,
    released: bool,
}

impl LockGuard {
    /// Acquire a lock and return a guard that auto-releases on drop.
    pub async fn acquire(
        lock: Arc<dyn Lock>,
        namespace: &str,
        resource_id: &str,
    ) -> Result<Self> {
        lock.acquire(namespace, resource_id).await?;
        Ok(Self {
            lock,
            namespace: namespace.to_string(),
            resource_id: resource_id.to_string(),
            released: false,
        })
    }

    /// Try to acquire a lock. Returns `None` if already held.
    pub async fn try_acquire(
        lock: Arc<dyn Lock>,
        namespace: &str,
        resource_id: &str,
    ) -> Result<Option<Self>> {
        if lock.try_acquire(namespace, resource_id).await? {
            Ok(Some(Self {
                lock,
                namespace: namespace.to_string(),
                resource_id: resource_id.to_string(),
                released: false,
            }))
        } else {
            Ok(None)
        }
    }

    /// Manually release the lock before the guard is dropped.
    pub async fn release(mut self) -> Result<bool> {
        self.released = true;
        self.lock.release(&self.namespace, &self.resource_id).await
    }
}

impl Drop for LockGuard {
    fn drop(&mut self) {
        if !self.released {
            let lock = self.lock.clone();
            let namespace = self.namespace.clone();
            let resource_id = self.resource_id.clone();

            let Some(handle) = tokio::runtime::Handle::try_current().ok() else {
                tracing::warn!(
                    namespace = %namespace,
                    resource_id = %resource_id,
                    "Cannot release lock: no tokio runtime in Drop"
                );
                return;
            };
            handle.spawn(async move {
                if let Err(e) = lock.release(&namespace, &resource_id).await {
                    tracing::error!(
                        namespace = %namespace,
                        resource_id = %resource_id,
                        error = %e,
                        "failed to release lock on drop"
                    );
                }
            });
        }
    }
}