llm_optimizer_processor/state/
postgres_lock.rs1use async_trait::async_trait;
55use chrono::Utc;
56use sqlx::{postgres::PgPoolOptions, PgPool, Row};
57use std::collections::hash_map::DefaultHasher;
58use std::hash::{Hash, Hasher};
59use std::sync::Arc;
60use std::time::Duration;
61use tokio::sync::RwLock;
62use tracing::{debug, warn};
63
64use super::distributed_lock::{DistributedLock, LockGuard, LockToken};
65use crate::error::{StateError, StateResult};
66
67pub struct PostgresDistributedLock {
73 pool: PgPool,
75
76 metadata: Arc<RwLock<std::collections::HashMap<String, (i64, LockToken, i64)>>>,
79
80 _monitor_handle: Option<tokio::task::JoinHandle<()>>,
82}
83
84impl std::fmt::Debug for PostgresDistributedLock {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("PostgresDistributedLock")
87 .field("metadata_count", &self.metadata.try_read().map(|m| m.len()))
88 .finish()
89 }
90}
91
92impl PostgresDistributedLock {
93 pub async fn new(database_url: &str) -> StateResult<Self> {
104 Self::with_pool_size(database_url, 10).await
105 }
106
107 pub async fn with_pool_size(database_url: &str, max_connections: u32) -> StateResult<Self> {
114 let pool = PgPoolOptions::new()
115 .max_connections(max_connections)
116 .connect(database_url)
117 .await
118 .map_err(|e| StateError::StorageError {
119 backend_type: "PostgreSQL".to_string(),
120 details: format!("Failed to connect to database: {}", e),
121 })?;
122
123 let metadata = Arc::new(RwLock::new(std::collections::HashMap::new()));
124
125 let monitor_handle = Self::start_ttl_monitor(metadata.clone(), pool.clone());
127
128 Ok(Self {
129 pool,
130 metadata,
131 _monitor_handle: Some(monitor_handle),
132 })
133 }
134
135 fn start_ttl_monitor(
137 metadata: Arc<RwLock<std::collections::HashMap<String, (i64, LockToken, i64)>>>,
138 pool: PgPool,
139 ) -> tokio::task::JoinHandle<()> {
140 tokio::spawn(async move {
141 let mut interval = tokio::time::interval(Duration::from_secs(1));
142
143 loop {
144 interval.tick().await;
145
146 let now = Utc::now().timestamp_millis();
147 let mut metadata = metadata.write().await;
148 let mut expired = Vec::new();
149
150 for (resource, (lock_id, token, expiry)) in metadata.iter() {
152 if *expiry <= now {
153 expired.push((resource.clone(), *lock_id, token.clone()));
154 }
155 }
156
157 for (resource, lock_id, token) in expired {
159 debug!(
160 resource = %resource,
161 lock_id = lock_id,
162 token = %token,
163 "Releasing expired lock"
164 );
165
166 let result = sqlx::query("SELECT pg_advisory_unlock($1)")
168 .bind(lock_id)
169 .execute(&pool)
170 .await;
171
172 if let Err(e) = result {
173 warn!(
174 resource = %resource,
175 error = %e,
176 "Failed to release expired lock"
177 );
178 }
179
180 metadata.remove(&resource);
181 }
182 }
183 })
184 }
185
186 fn resource_to_lock_id(resource: &str) -> i64 {
191 let mut hasher = DefaultHasher::new();
192 resource.hash(&mut hasher);
193 hasher.finish() as i64
194 }
195
196 fn calculate_expiry(ttl: Duration) -> i64 {
198 Utc::now().timestamp_millis() + ttl.as_millis() as i64
199 }
200}
201
202#[async_trait]
203impl DistributedLock for PostgresDistributedLock {
204 async fn acquire(&self, resource: &str, ttl: Duration) -> StateResult<LockGuard> {
205 let lock_id = Self::resource_to_lock_id(resource);
206 let token = LockToken::new();
207 let expiry = Self::calculate_expiry(ttl);
208
209 debug!(
210 resource = %resource,
211 lock_id = lock_id,
212 token = %token,
213 ttl_ms = ttl.as_millis(),
214 "Attempting to acquire PostgreSQL advisory lock"
215 );
216
217 let result = sqlx::query("SELECT pg_advisory_lock($1)")
220 .bind(lock_id)
221 .execute(&self.pool)
222 .await
223 .map_err(|e| StateError::StorageError {
224 backend_type: "PostgreSQL".to_string(),
225 details: format!("Failed to acquire advisory lock: {}", e),
226 })?;
227
228 debug!(
229 resource = %resource,
230 lock_id = lock_id,
231 "Advisory lock acquired"
232 );
233
234 {
236 let mut metadata = self.metadata.write().await;
237 metadata.insert(resource.to_string(), (lock_id, token.clone(), expiry));
238 }
239
240 let guard = LockGuard::new(
241 resource.to_string(),
242 token,
243 Utc::now(),
244 ttl,
245 Arc::new(Self {
246 pool: self.pool.clone(),
247 metadata: self.metadata.clone(),
248 _monitor_handle: None, }),
250 );
251
252 Ok(guard)
253 }
254
255 async fn try_acquire(&self, resource: &str, ttl: Duration) -> StateResult<Option<LockGuard>> {
256 let lock_id = Self::resource_to_lock_id(resource);
257 let token = LockToken::new();
258 let expiry = Self::calculate_expiry(ttl);
259
260 debug!(
261 resource = %resource,
262 lock_id = lock_id,
263 token = %token,
264 ttl_ms = ttl.as_millis(),
265 "Attempting non-blocking lock acquisition"
266 );
267
268 let row = sqlx::query("SELECT pg_try_advisory_lock($1) as acquired")
271 .bind(lock_id)
272 .fetch_one(&self.pool)
273 .await
274 .map_err(|e| StateError::StorageError {
275 backend_type: "PostgreSQL".to_string(),
276 details: format!("Failed to try advisory lock: {}", e),
277 })?;
278
279 let acquired: bool = row.get("acquired");
280
281 if acquired {
282 debug!(
283 resource = %resource,
284 lock_id = lock_id,
285 "Advisory lock acquired"
286 );
287
288 {
290 let mut metadata = self.metadata.write().await;
291 metadata.insert(resource.to_string(), (lock_id, token.clone(), expiry));
292 }
293
294 let guard = LockGuard::new(
295 resource.to_string(),
296 token,
297 Utc::now(),
298 ttl,
299 Arc::new(Self {
300 pool: self.pool.clone(),
301 metadata: self.metadata.clone(),
302 _monitor_handle: None,
303 }),
304 );
305
306 Ok(Some(guard))
307 } else {
308 debug!(resource = %resource, "Lock already held");
309 Ok(None)
310 }
311 }
312
313 async fn release_internal(&self, resource: &str, token: &LockToken) -> StateResult<()> {
314 debug!(resource = %resource, token = %token, "Releasing lock");
315
316 let lock_id = {
318 let mut metadata = self.metadata.write().await;
319
320 match metadata.get(resource) {
321 Some((lock_id, stored_token, _)) => {
322 if stored_token != token {
323 warn!(
324 resource = %resource,
325 token = %token,
326 "Token mismatch during release"
327 );
328 return Err(StateError::TransactionFailed {
329 operation: "release_lock".to_string(),
330 reason: "Token mismatch".to_string(),
331 });
332 }
333
334 let lock_id = *lock_id;
335 metadata.remove(resource);
336 lock_id
337 }
338 None => {
339 warn!(resource = %resource, "Lock not found in metadata");
340 return Err(StateError::KeyNotFound {
341 key: resource.to_string(),
342 });
343 }
344 }
345 };
346
347 let row = sqlx::query("SELECT pg_advisory_unlock($1) as released")
349 .bind(lock_id)
350 .fetch_one(&self.pool)
351 .await
352 .map_err(|e| StateError::StorageError {
353 backend_type: "PostgreSQL".to_string(),
354 details: format!("Failed to release advisory lock: {}", e),
355 })?;
356
357 let released: bool = row.get("released");
358
359 if !released {
360 warn!(
361 resource = %resource,
362 lock_id = lock_id,
363 "Advisory lock was not held"
364 );
365 return Err(StateError::TransactionFailed {
366 operation: "release_lock".to_string(),
367 reason: "Lock was not held".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 metadata = self.metadata.read().await;
377 let is_locked = metadata.contains_key(resource);
378
379 if is_locked {
381 if let Some((_, _, expiry)) = metadata.get(resource) {
382 let now = Utc::now().timestamp_millis();
383 return Ok(*expiry > now);
384 }
385 }
386
387 Ok(is_locked)
388 }
389
390 async fn extend(&self, guard: &mut LockGuard, ttl: Duration) -> StateResult<()> {
391 let new_expiry = Self::calculate_expiry(ttl);
392
393 debug!(
394 resource = %guard.resource(),
395 token = %guard.token(),
396 ttl_ms = ttl.as_millis(),
397 "Extending lock"
398 );
399
400 {
402 let mut metadata = self.metadata.write().await;
403
404 match metadata.get_mut(guard.resource()) {
405 Some((_, stored_token, expiry)) => {
406 if stored_token != guard.token() {
407 warn!(
408 resource = %guard.resource(),
409 "Token mismatch during extension"
410 );
411 return Err(StateError::TransactionFailed {
412 operation: "extend_lock".to_string(),
413 reason: "Token mismatch".to_string(),
414 });
415 }
416
417 *expiry = new_expiry;
418 }
419 None => {
420 warn!(resource = %guard.resource(), "Lock not found during extension");
421 return Err(StateError::KeyNotFound {
422 key: guard.resource().to_string(),
423 });
424 }
425 }
426 }
427
428 debug!(resource = %guard.resource(), "Lock extended");
429 Ok(())
430 }
431
432 async fn get_lock_info(&self, resource: &str) -> StateResult<Option<LockToken>> {
433 let metadata = self.metadata.read().await;
434
435 if let Some((_, token, expiry)) = metadata.get(resource) {
436 let now = Utc::now().timestamp_millis();
437 if *expiry > now {
438 return Ok(Some(token.clone()));
439 }
440 }
441
442 Ok(None)
443 }
444}
445
446impl Drop for PostgresDistributedLock {
447 fn drop(&mut self) {
448 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 async fn create_test_lock() -> Option<PostgresDistributedLock> {
458 match PostgresDistributedLock::new("postgresql://localhost/test").await {
460 Ok(lock) => Some(lock),
461 Err(_) => {
462 eprintln!("Skipping PostgreSQL tests - PostgreSQL not available");
463 None
464 }
465 }
466 }
467
468 #[tokio::test]
469 async fn test_acquire_and_release() {
470 let Some(lock) = create_test_lock().await else {
471 return;
472 };
473
474 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
475
476 assert!(!lock.is_locked(&resource).await.unwrap());
478
479 let guard = lock
481 .acquire(&resource, Duration::from_secs(10))
482 .await
483 .unwrap();
484
485 assert!(lock.is_locked(&resource).await.unwrap());
487
488 lock.release(guard).await.unwrap();
490
491 tokio::time::sleep(Duration::from_millis(100)).await;
493
494 assert!(!lock.is_locked(&resource).await.unwrap());
496 }
497
498 #[tokio::test]
499 async fn test_try_acquire() {
500 let Some(lock) = create_test_lock().await else {
501 return;
502 };
503
504 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
505
506 let guard1 = lock
508 .try_acquire(&resource, Duration::from_secs(10))
509 .await
510 .unwrap();
511 assert!(guard1.is_some());
512
513 let guard2 = lock
515 .try_acquire(&resource, Duration::from_secs(10))
516 .await
517 .unwrap();
518 assert!(guard2.is_none());
519
520 drop(guard1);
522
523 tokio::time::sleep(Duration::from_millis(100)).await;
525
526 let guard3 = lock
528 .try_acquire(&resource, Duration::from_secs(10))
529 .await
530 .unwrap();
531 assert!(guard3.is_some());
532 }
533
534 #[tokio::test]
535 async fn test_lock_extension() {
536 let Some(lock) = create_test_lock().await else {
537 return;
538 };
539
540 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
541
542 let mut guard = lock
544 .acquire(&resource, Duration::from_secs(2))
545 .await
546 .unwrap();
547
548 tokio::time::sleep(Duration::from_millis(1500)).await;
550
551 lock.extend(&mut guard, Duration::from_secs(5))
553 .await
554 .unwrap();
555
556 tokio::time::sleep(Duration::from_millis(1000)).await;
558
559 assert!(lock.is_locked(&resource).await.unwrap());
561 }
562
563 #[tokio::test]
564 async fn test_lock_expiration() {
565 let Some(lock) = create_test_lock().await else {
566 return;
567 };
568
569 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
570
571 let guard = lock
573 .acquire(&resource, Duration::from_secs(1))
574 .await
575 .unwrap();
576
577 assert!(lock.is_locked(&resource).await.unwrap());
578
579 std::mem::forget(guard);
581
582 tokio::time::sleep(Duration::from_secs(3)).await;
584
585 assert!(!lock.is_locked(&resource).await.unwrap());
587 }
588
589 #[tokio::test]
590 async fn test_get_lock_info() {
591 let Some(lock) = create_test_lock().await else {
592 return;
593 };
594
595 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
596
597 assert!(lock.get_lock_info(&resource).await.unwrap().is_none());
599
600 let guard = lock
602 .acquire(&resource, Duration::from_secs(10))
603 .await
604 .unwrap();
605
606 let token = lock.get_lock_info(&resource).await.unwrap();
608 assert!(token.is_some());
609 assert_eq!(token.unwrap(), *guard.token());
610 }
611
612 #[test]
613 fn test_resource_to_lock_id() {
614 let id1 = PostgresDistributedLock::resource_to_lock_id("resource1");
615 let id2 = PostgresDistributedLock::resource_to_lock_id("resource2");
616 let id3 = PostgresDistributedLock::resource_to_lock_id("resource1");
617
618 assert_eq!(id1, id3);
620
621 assert_ne!(id1, id2);
623 }
624
625 #[tokio::test]
626 async fn test_concurrent_access() {
627 let Some(lock) = create_test_lock().await else {
628 return;
629 };
630
631 let resource = format!("test:resource:{}", uuid::Uuid::new_v4());
632 let lock = Arc::new(lock);
633
634 let mut handles = vec![];
635
636 for i in 0..5 {
638 let lock = lock.clone();
639 let resource = resource.clone();
640
641 let handle = tokio::spawn(async move {
642 if let Some(guard) = lock
643 .try_acquire(&resource, Duration::from_secs(1))
644 .await
645 .unwrap()
646 {
647 tokio::time::sleep(Duration::from_millis(100)).await;
649 drop(guard);
650 Some(i)
651 } else {
652 None
653 }
654 });
655
656 handles.push(handle);
657 }
658
659 let mut successful = 0;
661 for handle in handles {
662 if handle.await.unwrap().is_some() {
663 successful += 1;
664 }
665 }
666
667 assert!(successful >= 1);
669 }
670}