Skip to main content

llm_optimizer_processor/state/
distributed_lock.rs

1//! Distributed locking for coordinating state access across processor instances
2//!
3//! This module provides distributed locking mechanisms to coordinate critical operations
4//! across multiple processor instances, preventing race conditions in distributed scenarios.
5//!
6//! ## Key Features
7//!
8//! - **Multiple Backends**: Redis and PostgreSQL implementations
9//! - **RAII Pattern**: Automatic lock release via `LockGuard` drop
10//! - **Lock Extension**: Extend TTL for long-running operations
11//! - **Deadlock Detection**: Automatic expiration via TTL
12//! - **Fair Locking**: FIFO queue for waiting locks (Redis)
13//! - **Atomic Operations**: LUA scripts and database transactions
14//!
15//! ## Use Cases
16//!
17//! - **Checkpoint Coordination**: Only one instance checkpoints at a time
18//! - **State Compaction**: Prevent concurrent compaction operations
19//! - **Leader Election**: Single coordinator for distributed tasks
20//! - **Critical Section Protection**: Serialize access to shared resources
21//!
22//! ## Example Usage
23//!
24//! ```rust,no_run
25//! use processor::state::distributed_lock::{RedisDistributedLock, DistributedLock};
26//! use std::time::Duration;
27//!
28//! #[tokio::main]
29//! async fn main() -> anyhow::Result<()> {
30//!     // Create Redis-based distributed lock
31//!     let lock = RedisDistributedLock::new("redis://localhost:6379").await?;
32//!
33//!     // Acquire lock with 30-second TTL
34//!     let guard = lock.acquire("checkpoint:state1", Duration::from_secs(30)).await?;
35//!
36//!     // Perform critical operation
37//!     println!("Performing checkpoint...");
38//!
39//!     // Lock is automatically released when guard is dropped
40//!     drop(guard);
41//!
42//!     Ok(())
43//! }
44//! ```
45//!
46//! ## Lock Extension
47//!
48//! For long-running operations, extend the lock TTL:
49//!
50//! ```rust,no_run
51//! # use processor::state::distributed_lock::{RedisDistributedLock, DistributedLock};
52//! # use std::time::Duration;
53//! # #[tokio::main]
54//! # async fn main() -> anyhow::Result<()> {
55//! # let lock = RedisDistributedLock::new("redis://localhost:6379").await?;
56//! let mut guard = lock.acquire("compaction:state1", Duration::from_secs(30)).await?;
57//!
58//! // Start long-running operation
59//! for i in 0..10 {
60//!     // Do work...
61//!     tokio::time::sleep(Duration::from_secs(10)).await;
62//!
63//!     // Extend lock before it expires
64//!     lock.extend(&mut guard, Duration::from_secs(30)).await?;
65//! }
66//! # Ok(())
67//! # }
68//! ```
69
70use async_trait::async_trait;
71use chrono::{DateTime, Utc};
72use std::sync::Arc;
73use std::time::Duration;
74use uuid::Uuid;
75
76use crate::error::StateResult;
77
78/// Lock ownership token
79///
80/// A unique identifier for a lock instance, used to verify ownership
81/// during extension and release operations.
82#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub struct LockToken(String);
84
85impl LockToken {
86    /// Create a new random lock token
87    pub fn new() -> Self {
88        Self(Uuid::new_v4().to_string())
89    }
90
91    /// Get the token value
92    pub fn value(&self) -> &str {
93        &self.0
94    }
95
96    /// Create from an existing string (for deserialization)
97    pub fn from_string(s: String) -> Self {
98        Self(s)
99    }
100}
101
102impl Default for LockToken {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl std::fmt::Display for LockToken {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}", self.0)
111    }
112}
113
114/// RAII guard for distributed locks
115///
116/// Automatically releases the lock when dropped. Contains metadata about
117/// the lock for verification and monitoring purposes.
118#[derive(Debug)]
119pub struct LockGuard {
120    /// Resource being locked
121    resource: String,
122
123    /// Ownership token
124    token: LockToken,
125
126    /// When the lock was acquired
127    acquired_at: DateTime<Utc>,
128
129    /// Time-to-live for the lock
130    ttl: Duration,
131
132    /// Reference to the lock backend for cleanup
133    backend: Arc<dyn DistributedLock>,
134}
135
136impl LockGuard {
137    /// Create a new lock guard
138    pub fn new(
139        resource: String,
140        token: LockToken,
141        acquired_at: DateTime<Utc>,
142        ttl: Duration,
143        backend: Arc<dyn DistributedLock>,
144    ) -> Self {
145        Self {
146            resource,
147            token,
148            acquired_at,
149            ttl,
150            backend,
151        }
152    }
153
154    /// Get the locked resource name
155    pub fn resource(&self) -> &str {
156        &self.resource
157    }
158
159    /// Get the lock token
160    pub fn token(&self) -> &LockToken {
161        &self.token
162    }
163
164    /// Get the acquisition timestamp
165    pub fn acquired_at(&self) -> DateTime<Utc> {
166        self.acquired_at
167    }
168
169    /// Get the lock TTL
170    pub fn ttl(&self) -> Duration {
171        self.ttl
172    }
173
174    /// Check if the lock has expired (based on TTL)
175    pub fn is_expired(&self) -> bool {
176        let elapsed = Utc::now().signed_duration_since(self.acquired_at);
177        elapsed.to_std().unwrap_or_default() >= self.ttl
178    }
179
180    /// Get time remaining before expiration
181    pub fn time_remaining(&self) -> Duration {
182        let elapsed = Utc::now().signed_duration_since(self.acquired_at);
183        let elapsed_std = elapsed.to_std().unwrap_or_default();
184
185        if elapsed_std >= self.ttl {
186            Duration::from_secs(0)
187        } else {
188            self.ttl - elapsed_std
189        }
190    }
191
192    /// Manually release the lock early
193    ///
194    /// This consumes the guard and releases the lock. Normally not needed
195    /// as the lock is released automatically on drop.
196    pub async fn release(self) -> StateResult<()> {
197        // Release happens automatically via Drop, but we can force it here
198        drop(self);
199        Ok(())
200    }
201}
202
203impl Drop for LockGuard {
204    fn drop(&mut self) {
205        // Schedule lock release in background
206        // We can't await in drop, so we spawn a task
207        let backend = self.backend.clone();
208        let resource = self.resource.clone();
209        let token = self.token.clone();
210
211        tokio::spawn(async move {
212            // Best effort release - ignore errors as lock will expire anyway
213            let _ = backend.release_internal(&resource, &token).await;
214        });
215    }
216}
217
218/// Distributed lock trait
219///
220/// Defines the interface for distributed locking implementations.
221/// All operations are async and use the RAII pattern via `LockGuard`.
222#[async_trait]
223pub trait DistributedLock: Send + Sync + std::fmt::Debug {
224    /// Acquire a lock for the given resource
225    ///
226    /// Blocks until the lock is acquired or an error occurs.
227    /// The lock will automatically expire after the TTL.
228    ///
229    /// # Arguments
230    ///
231    /// * `resource` - Name of the resource to lock
232    /// * `ttl` - Time-to-live for the lock
233    ///
234    /// # Returns
235    ///
236    /// * `Ok(LockGuard)` - Lock acquired successfully
237    /// * `Err(StateError)` - Failed to acquire lock
238    ///
239    /// # Example
240    ///
241    /// ```rust,no_run
242    /// # use processor::state::distributed_lock::{RedisDistributedLock, DistributedLock};
243    /// # use std::time::Duration;
244    /// # #[tokio::main]
245    /// # async fn main() -> anyhow::Result<()> {
246    /// # let lock = RedisDistributedLock::new("redis://localhost:6379").await?;
247    /// let guard = lock.acquire("checkpoint", Duration::from_secs(30)).await?;
248    /// // Critical section
249    /// drop(guard); // Lock released
250    /// # Ok(())
251    /// # }
252    /// ```
253    async fn acquire(&self, resource: &str, ttl: Duration) -> StateResult<LockGuard>;
254
255    /// Try to acquire a lock without blocking
256    ///
257    /// Returns immediately with success or failure. Useful for non-blocking scenarios.
258    ///
259    /// # Arguments
260    ///
261    /// * `resource` - Name of the resource to lock
262    /// * `ttl` - Time-to-live for the lock
263    ///
264    /// # Returns
265    ///
266    /// * `Ok(Some(LockGuard))` - Lock acquired
267    /// * `Ok(None)` - Lock already held by another instance
268    /// * `Err(StateError)` - Backend error
269    async fn try_acquire(&self, resource: &str, ttl: Duration) -> StateResult<Option<LockGuard>>;
270
271    /// Release a lock
272    ///
273    /// Normally not needed as locks are released automatically when the guard
274    /// is dropped. This is provided for explicit control.
275    ///
276    /// # Arguments
277    ///
278    /// * `guard` - The lock guard to release
279    ///
280    /// # Returns
281    ///
282    /// * `Ok(())` - Lock released
283    /// * `Err(StateError)` - Failed to release (token mismatch, already released)
284    async fn release(&self, guard: LockGuard) -> StateResult<()> {
285        self.release_internal(guard.resource(), guard.token()).await
286    }
287
288    /// Internal release method (used by Drop)
289    async fn release_internal(&self, resource: &str, token: &LockToken) -> StateResult<()>;
290
291    /// Check if a resource is currently locked
292    ///
293    /// # Arguments
294    ///
295    /// * `resource` - Name of the resource to check
296    ///
297    /// # Returns
298    ///
299    /// * `Ok(true)` - Resource is locked
300    /// * `Ok(false)` - Resource is not locked
301    /// * `Err(StateError)` - Backend error
302    async fn is_locked(&self, resource: &str) -> StateResult<bool>;
303
304    /// Extend the TTL of an existing lock
305    ///
306    /// Useful for long-running operations that might exceed the initial TTL.
307    /// The guard's TTL is updated on success.
308    ///
309    /// # Arguments
310    ///
311    /// * `guard` - The lock guard to extend
312    /// * `ttl` - New TTL from current time
313    ///
314    /// # Returns
315    ///
316    /// * `Ok(())` - TTL extended
317    /// * `Err(StateError)` - Failed to extend (token mismatch, lock expired)
318    ///
319    /// # Example
320    ///
321    /// ```rust,no_run
322    /// # use processor::state::distributed_lock::{RedisDistributedLock, DistributedLock};
323    /// # use std::time::Duration;
324    /// # #[tokio::main]
325    /// # async fn main() -> anyhow::Result<()> {
326    /// # let lock = RedisDistributedLock::new("redis://localhost:6379").await?;
327    /// let mut guard = lock.acquire("long_operation", Duration::from_secs(30)).await?;
328    ///
329    /// // Long operation...
330    /// tokio::time::sleep(Duration::from_secs(20)).await;
331    ///
332    /// // Extend before expiration
333    /// lock.extend(&mut guard, Duration::from_secs(30)).await?;
334    /// # Ok(())
335    /// # }
336    /// ```
337    async fn extend(&self, guard: &mut LockGuard, ttl: Duration) -> StateResult<()>;
338
339    /// Get information about who holds a lock
340    ///
341    /// Returns the token of the current lock holder, if any.
342    ///
343    /// # Arguments
344    ///
345    /// * `resource` - Name of the resource to query
346    ///
347    /// # Returns
348    ///
349    /// * `Ok(Some(LockToken))` - Lock is held
350    /// * `Ok(None)` - Lock is not held
351    /// * `Err(StateError)` - Backend error
352    async fn get_lock_info(&self, resource: &str) -> StateResult<Option<LockToken>>;
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_lock_token_creation() {
361        let token1 = LockToken::new();
362        let token2 = LockToken::new();
363
364        assert_ne!(token1, token2);
365        assert!(!token1.value().is_empty());
366    }
367
368    #[test]
369    fn test_lock_token_from_string() {
370        let original = "test-token-123";
371        let token = LockToken::from_string(original.to_string());
372
373        assert_eq!(token.value(), original);
374    }
375
376    #[test]
377    fn test_lock_token_display() {
378        let token = LockToken::from_string("test-123".to_string());
379        assert_eq!(format!("{}", token), "test-123");
380    }
381
382    // Note: LockGuard tests require a backend implementation
383    // See redis_lock.rs and postgres_lock.rs for integration tests
384}