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}