Skip to main content

ppoppo_infra/
lock.rs

1//! Backend-agnostic distributed lock trait.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6
7use crate::Result;
8
9/// Distributed lock operations.
10///
11/// Session-level locking with backend-agnostic RAII via [`LockGuard`].
12/// Transaction-level locks remain backend-specific.
13#[async_trait]
14pub trait Lock: Send + Sync {
15    /// Acquire a lock (blocking). Blocks until the lock is acquired.
16    async fn acquire(&self, namespace: &str, resource_id: &str) -> Result<()>;
17
18    /// Try to acquire a lock (non-blocking).
19    ///
20    /// Returns `true` if lock acquired, `false` if already held.
21    async fn try_acquire(&self, namespace: &str, resource_id: &str) -> Result<bool>;
22
23    /// Release a lock. Returns `true` if lock was held and released.
24    async fn release(&self, namespace: &str, resource_id: &str) -> Result<bool>;
25
26    /// Acquire a lock with timeout.
27    ///
28    /// Retries acquiring the lock until timeout is reached.
29    async fn acquire_with_timeout(
30        &self,
31        namespace: &str,
32        resource_id: &str,
33        timeout_ms: i32,
34        retry_interval_ms: u64,
35    ) -> Result<()>;
36}
37
38/// Backend-agnostic RAII lock guard.
39///
40/// Wraps `Arc<dyn Lock>` and releases via the trait's `release()` method.
41/// On drop without explicit release, spawns a tokio task for async cleanup.
42pub struct LockGuard {
43    lock: Arc<dyn Lock>,
44    namespace: String,
45    resource_id: String,
46    released: bool,
47}
48
49impl LockGuard {
50    /// Acquire a lock and return a guard that auto-releases on drop.
51    pub async fn acquire(
52        lock: Arc<dyn Lock>,
53        namespace: &str,
54        resource_id: &str,
55    ) -> Result<Self> {
56        lock.acquire(namespace, resource_id).await?;
57        Ok(Self {
58            lock,
59            namespace: namespace.to_string(),
60            resource_id: resource_id.to_string(),
61            released: false,
62        })
63    }
64
65    /// Try to acquire a lock. Returns `None` if already held.
66    pub async fn try_acquire(
67        lock: Arc<dyn Lock>,
68        namespace: &str,
69        resource_id: &str,
70    ) -> Result<Option<Self>> {
71        if lock.try_acquire(namespace, resource_id).await? {
72            Ok(Some(Self {
73                lock,
74                namespace: namespace.to_string(),
75                resource_id: resource_id.to_string(),
76                released: false,
77            }))
78        } else {
79            Ok(None)
80        }
81    }
82
83    /// Manually release the lock before the guard is dropped.
84    pub async fn release(mut self) -> Result<bool> {
85        self.released = true;
86        self.lock.release(&self.namespace, &self.resource_id).await
87    }
88}
89
90impl Drop for LockGuard {
91    fn drop(&mut self) {
92        if !self.released {
93            let lock = self.lock.clone();
94            let namespace = self.namespace.clone();
95            let resource_id = self.resource_id.clone();
96
97            let Some(handle) = tokio::runtime::Handle::try_current().ok() else {
98                tracing::warn!(
99                    namespace = %namespace,
100                    resource_id = %resource_id,
101                    "Cannot release lock: no tokio runtime in Drop"
102                );
103                return;
104            };
105            handle.spawn(async move {
106                if let Err(e) = lock.release(&namespace, &resource_id).await {
107                    tracing::error!(
108                        namespace = %namespace,
109                        resource_id = %resource_id,
110                        error = %e,
111                        "failed to release lock on drop"
112                    );
113                }
114            });
115        }
116    }
117}