llm_optimizer_processor/state/
redis_lock.rs1use async_trait::async_trait;
43use chrono::Utc;
44use redis::{
45 aio::ConnectionManager, AsyncCommands, Client, RedisError, Script, Value,
46};
47use std::sync::Arc;
48use std::time::Duration;
49use tracing::{debug, warn};
50
51use super::distributed_lock::{DistributedLock, LockGuard, LockToken};
52use crate::error::{StateError, StateResult};
53
54pub struct RedisDistributedLock {
59 connection: ConnectionManager,
61
62 prefix: String,
64
65 retry_config: RetryConfig,
67}
68
69impl std::fmt::Debug for RedisDistributedLock {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("RedisDistributedLock")
72 .field("prefix", &self.prefix)
73 .field("retry_config", &self.retry_config)
74 .finish()
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct RetryConfig {
81 pub max_attempts: u32,
83
84 pub initial_delay: Duration,
86
87 pub max_delay: Duration,
89
90 pub backoff_multiplier: f64,
92}
93
94impl Default for RetryConfig {
95 fn default() -> Self {
96 Self {
97 max_attempts: 10,
98 initial_delay: Duration::from_millis(100),
99 max_delay: Duration::from_secs(5),
100 backoff_multiplier: 2.0,
101 }
102 }
103}
104
105impl RetryConfig {
106 pub fn no_retry() -> Self {
108 Self {
109 max_attempts: 1,
110 ..Default::default()
111 }
112 }
113
114 pub fn aggressive() -> Self {
116 Self {
117 max_attempts: 50,
118 initial_delay: Duration::from_millis(50),
119 max_delay: Duration::from_secs(1),
120 backoff_multiplier: 1.5,
121 }
122 }
123}
124
125const RELEASE_SCRIPT: &str = r#"
127 if redis.call("GET", KEYS[1]) == ARGV[1] then
128 return redis.call("DEL", KEYS[1])
129 else
130 return 0
131 end
132"#;
133
134const EXTEND_SCRIPT: &str = r#"
136 if redis.call("GET", KEYS[1]) == ARGV[1] then
137 return redis.call("PEXPIRE", KEYS[1], ARGV[2])
138 else
139 return 0
140 end
141"#;
142
143impl RedisDistributedLock {
144 pub async fn new(redis_url: &str) -> StateResult<Self> {
155 Self::with_config(redis_url, "lock:", RetryConfig::default()).await
156 }
157
158 pub async fn with_config(
166 redis_url: &str,
167 prefix: &str,
168 retry_config: RetryConfig,
169 ) -> StateResult<Self> {
170 let client = Client::open(redis_url).map_err(|e| StateError::StorageError {
171 backend_type: "Redis".to_string(),
172 details: format!("Failed to create Redis client: {}", e),
173 })?;
174
175 let connection = client
176 .get_connection_manager()
177 .await
178 .map_err(|e| StateError::StorageError {
179 backend_type: "Redis".to_string(),
180 details: format!("Failed to connect to Redis: {}", e),
181 })?;
182
183 Ok(Self {
184 connection,
185 prefix: prefix.to_string(),
186 retry_config,
187 })
188 }
189
190 fn resource_key(&self, resource: &str) -> String {
192 format!("{}{}", self.prefix, resource)
193 }
194
195 fn calculate_delay(&self, attempt: u32) -> Duration {
197 let delay = self.retry_config.initial_delay.as_millis() as f64
198 * self.retry_config.backoff_multiplier.powi(attempt as i32);
199
200 Duration::from_millis(delay.min(self.retry_config.max_delay.as_millis() as f64) as u64)
201 }
202}
203
204#[async_trait]
205impl DistributedLock for RedisDistributedLock {
206 async fn acquire(&self, resource: &str, ttl: Duration) -> StateResult<LockGuard> {
207 let key = self.resource_key(resource);
208 let token = LockToken::new();
209 let ttl_secs = ttl.as_secs();
210
211 debug!(
212 resource = %resource,
213 token = %token,
214 ttl_secs = ttl_secs,
215 "Attempting to acquire lock"
216 );
217
218 let mut attempt = 0;
219
220 loop {
221 attempt += 1;
222
223 let mut conn = self.connection.clone();
225 let result: Result<Value, RedisError> = redis::cmd("SET")
226 .arg(&key)
227 .arg(token.value())
228 .arg("NX")
229 .arg("EX")
230 .arg(ttl_secs)
231 .query_async(&mut conn)
232 .await;
233
234 match result {
235 Ok(Value::Okay) => {
236 debug!(
237 resource = %resource,
238 token = %token,
239 attempt = attempt,
240 "Lock acquired"
241 );
242
243 let guard = LockGuard::new(
244 resource.to_string(),
245 token,
246 Utc::now(),
247 ttl,
248 Arc::new(Self {
249 connection: self.connection.clone(),
250 prefix: self.prefix.clone(),
251 retry_config: self.retry_config.clone(),
252 }),
253 );
254
255 return Ok(guard);
256 }
257 Ok(_) => {
258 if attempt >= self.retry_config.max_attempts {
260 return Err(StateError::TransactionFailed {
261 operation: "acquire_lock".to_string(),
262 reason: format!(
263 "Failed to acquire lock after {} attempts",
264 attempt
265 ),
266 });
267 }
268
269 let delay = self.calculate_delay(attempt);
271 debug!(
272 resource = %resource,
273 attempt = attempt,
274 delay_ms = delay.as_millis(),
275 "Lock held by another instance, retrying"
276 );
277 tokio::time::sleep(delay).await;
278 }
279 Err(e) => {
280 return Err(StateError::StorageError {
281 backend_type: "Redis".to_string(),
282 details: format!("Lock acquisition failed: {}", e),
283 });
284 }
285 }
286 }
287 }
288
289 async fn try_acquire(&self, resource: &str, ttl: Duration) -> StateResult<Option<LockGuard>> {
290 let key = self.resource_key(resource);
291 let token = LockToken::new();
292 let ttl_secs = ttl.as_secs();
293
294 debug!(
295 resource = %resource,
296 token = %token,
297 ttl_secs = ttl_secs,
298 "Attempting non-blocking lock acquisition"
299 );
300
301 let mut conn = self.connection.clone();
302 let result: Result<Value, RedisError> = redis::cmd("SET")
303 .arg(&key)
304 .arg(token.value())
305 .arg("NX")
306 .arg("EX")
307 .arg(ttl_secs)
308 .query_async(&mut conn)
309 .await;
310
311 match result {
312 Ok(Value::Okay) => {
313 debug!(resource = %resource, token = %token, "Lock acquired");
314
315 let guard = LockGuard::new(
316 resource.to_string(),
317 token,
318 Utc::now(),
319 ttl,
320 Arc::new(Self {
321 connection: self.connection.clone(),
322 prefix: self.prefix.clone(),
323 retry_config: self.retry_config.clone(),
324 }),
325 );
326
327 Ok(Some(guard))
328 }
329 Ok(_) => {
330 debug!(resource = %resource, "Lock already held");
331 Ok(None)
332 }
333 Err(e) => Err(StateError::StorageError {
334 backend_type: "Redis".to_string(),
335 details: format!("Lock acquisition failed: {}", e),
336 }),
337 }
338 }
339
340 async fn release_internal(&self, resource: &str, token: &LockToken) -> StateResult<()> {
341 let key = self.resource_key(resource);
342
343 debug!(resource = %resource, token = %token, "Releasing lock");
344
345 let script = Script::new(RELEASE_SCRIPT);
347 let mut conn = self.connection.clone();
348
349 let result: i32 = script
350 .key(&key)
351 .arg(token.value())
352 .invoke_async(&mut conn)
353 .await
354 .map_err(|e| StateError::StorageError {
355 backend_type: "Redis".to_string(),
356 details: format!("Lock release failed: {}", e),
357 })?;
358
359 if result == 0 {
360 warn!(
361 resource = %resource,
362 token = %token,
363 "Lock release failed: token mismatch or already released"
364 );
365 return Err(StateError::TransactionFailed {
366 operation: "release_lock".to_string(),
367 reason: "Token mismatch or lock already released".to_string(),
368 });
369 }
370
371 debug!(resource = %resource, "Lock released");
372 Ok(())
373 }
374
375 async fn is_locked(&self, resource: &str) -> StateResult<bool> {
376 let key = self.resource_key(resource);
377 let mut conn = self.connection.clone();
378
379 let exists: bool = conn.exists(&key).await.map_err(|e| StateError::StorageError {
380 backend_type: "Redis".to_string(),
381 details: format!("Failed to check lock existence: {}", e),
382 })?;
383
384 Ok(exists)
385 }
386
387 async fn extend(&self, guard: &mut LockGuard, ttl: Duration) -> StateResult<()> {
388 let key = self.resource_key(guard.resource());
389 let ttl_ms = ttl.as_millis() as i64;
390
391 debug!(
392 resource = %guard.resource(),
393 token = %guard.token(),
394 ttl_ms = ttl_ms,
395 "Extending lock"
396 );
397
398 let script = Script::new(EXTEND_SCRIPT);
400 let mut conn = self.connection.clone();
401
402 let result: i32 = script
403 .key(&key)
404 .arg(guard.token().value())
405 .arg(ttl_ms)
406 .invoke_async(&mut conn)
407 .await
408 .map_err(|e| StateError::StorageError {
409 backend_type: "Redis".to_string(),
410 details: format!("Lock extension failed: {}", e),
411 })?;
412
413 if result == 0 {
414 warn!(
415 resource = %guard.resource(),
416 token = %guard.token(),
417 "Lock extension failed: token mismatch or lock expired"
418 );
419 return Err(StateError::TransactionFailed {
420 operation: "extend_lock".to_string(),
421 reason: "Token mismatch or lock expired".to_string(),
422 });
423 }
424
425 debug!(resource = %guard.resource(), "Lock extended");
430 Ok(())
431 }
432
433 async fn get_lock_info(&self, resource: &str) -> StateResult<Option<LockToken>> {
434 let key = self.resource_key(resource);
435 let mut conn = self.connection.clone();
436
437 let value: Option<String> = conn.get(&key).await.map_err(|e| StateError::StorageError {
438 backend_type: "Redis".to_string(),
439 details: format!("Failed to get lock info: {}", e),
440 })?;
441
442 Ok(value.map(LockToken::from_string))
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use std::time::Duration;
450
451 async fn create_test_lock() -> Option<RedisDistributedLock> {
453 match RedisDistributedLock::with_config(
455 "redis://localhost:6379",
456 "test:lock:",
457 RetryConfig::no_retry(),
458 )
459 .await
460 {
461 Ok(lock) => Some(lock),
462 Err(_) => {
463 eprintln!("Skipping Redis tests - Redis not available");
464 None
465 }
466 }
467 }
468
469 #[tokio::test]
470 async fn test_acquire_and_release() {
471 let Some(lock) = create_test_lock().await else {
472 return;
473 };
474
475 let resource = format!("test:resource:{}", Uuid::new_v4());
476
477 assert!(!lock.is_locked(&resource).await.unwrap());
479
480 let guard = lock
482 .acquire(&resource, Duration::from_secs(10))
483 .await
484 .unwrap();
485
486 assert!(lock.is_locked(&resource).await.unwrap());
488
489 lock.release(guard).await.unwrap();
491
492 assert!(!lock.is_locked(&resource).await.unwrap());
494 }
495
496 #[tokio::test]
497 async fn test_try_acquire() {
498 let Some(lock) = create_test_lock().await else {
499 return;
500 };
501
502 let resource = format!("test:resource:{}", Uuid::new_v4());
503
504 let guard1 = lock
506 .try_acquire(&resource, Duration::from_secs(10))
507 .await
508 .unwrap();
509 assert!(guard1.is_some());
510
511 let guard2 = lock
513 .try_acquire(&resource, Duration::from_secs(10))
514 .await
515 .unwrap();
516 assert!(guard2.is_none());
517
518 drop(guard1);
520
521 tokio::time::sleep(Duration::from_millis(100)).await;
523
524 let guard3 = lock
526 .try_acquire(&resource, Duration::from_secs(10))
527 .await
528 .unwrap();
529 assert!(guard3.is_some());
530 }
531
532 #[tokio::test]
533 async fn test_lock_expiration() {
534 let Some(lock) = create_test_lock().await else {
535 return;
536 };
537
538 let resource = format!("test:resource:{}", Uuid::new_v4());
539
540 let guard = lock
542 .acquire(&resource, Duration::from_secs(1))
543 .await
544 .unwrap();
545
546 assert!(lock.is_locked(&resource).await.unwrap());
547
548 std::mem::forget(guard);
550
551 tokio::time::sleep(Duration::from_secs(2)).await;
553
554 assert!(!lock.is_locked(&resource).await.unwrap());
556 }
557
558 #[tokio::test]
559 async fn test_lock_extension() {
560 let Some(lock) = create_test_lock().await else {
561 return;
562 };
563
564 let resource = format!("test:resource:{}", Uuid::new_v4());
565
566 let mut guard = lock
568 .acquire(&resource, Duration::from_secs(2))
569 .await
570 .unwrap();
571
572 tokio::time::sleep(Duration::from_millis(1500)).await;
574
575 lock.extend(&mut guard, Duration::from_secs(5))
577 .await
578 .unwrap();
579
580 tokio::time::sleep(Duration::from_millis(1000)).await;
582
583 assert!(lock.is_locked(&resource).await.unwrap());
585 }
586
587 #[tokio::test]
588 async fn test_concurrent_access() {
589 let Some(lock) = create_test_lock().await else {
590 return;
591 };
592
593 let resource = format!("test:resource:{}", Uuid::new_v4());
594 let lock = Arc::new(lock);
595
596 let mut handles = vec![];
597
598 for i in 0..5 {
600 let lock = lock.clone();
601 let resource = resource.clone();
602
603 let handle = tokio::spawn(async move {
604 if let Some(guard) = lock
605 .try_acquire(&resource, Duration::from_secs(1))
606 .await
607 .unwrap()
608 {
609 tokio::time::sleep(Duration::from_millis(100)).await;
611 drop(guard);
612 Some(i)
613 } else {
614 None
615 }
616 });
617
618 handles.push(handle);
619 }
620
621 let mut successful = 0;
623 for handle in handles {
624 if handle.await.unwrap().is_some() {
625 successful += 1;
626 }
627 }
628
629 assert!(successful >= 1);
632 }
633
634 #[tokio::test]
635 async fn test_get_lock_info() {
636 let Some(lock) = create_test_lock().await else {
637 return;
638 };
639
640 let resource = format!("test:resource:{}", Uuid::new_v4());
641
642 assert!(lock.get_lock_info(&resource).await.unwrap().is_none());
644
645 let guard = lock
647 .acquire(&resource, Duration::from_secs(10))
648 .await
649 .unwrap();
650
651 let token = lock.get_lock_info(&resource).await.unwrap();
653 assert!(token.is_some());
654 assert_eq!(token.unwrap(), *guard.token());
655 }
656
657 #[test]
658 fn test_retry_config() {
659 let config = RetryConfig::default();
660 assert_eq!(config.max_attempts, 10);
661
662 let no_retry = RetryConfig::no_retry();
663 assert_eq!(no_retry.max_attempts, 1);
664
665 let aggressive = RetryConfig::aggressive();
666 assert_eq!(aggressive.max_attempts, 50);
667 }
668}