use async_trait::async_trait;
use chrono::Utc;
use redis::{
aio::ConnectionManager, AsyncCommands, Client, RedisError, Script, Value,
};
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, warn};
use super::distributed_lock::{DistributedLock, LockGuard, LockToken};
use crate::error::{StateError, StateResult};
pub struct RedisDistributedLock {
connection: ConnectionManager,
prefix: String,
retry_config: RetryConfig,
}
impl std::fmt::Debug for RedisDistributedLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisDistributedLock")
.field("prefix", &self.prefix)
.field("retry_config", &self.retry_config)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 10,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(5),
backoff_multiplier: 2.0,
}
}
}
impl RetryConfig {
pub fn no_retry() -> Self {
Self {
max_attempts: 1,
..Default::default()
}
}
pub fn aggressive() -> Self {
Self {
max_attempts: 50,
initial_delay: Duration::from_millis(50),
max_delay: Duration::from_secs(1),
backoff_multiplier: 1.5,
}
}
}
const RELEASE_SCRIPT: &str = r#"
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"#;
const EXTEND_SCRIPT: &str = r#"
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
return 0
end
"#;
impl RedisDistributedLock {
pub async fn new(redis_url: &str) -> StateResult<Self> {
Self::with_config(redis_url, "lock:", RetryConfig::default()).await
}
pub async fn with_config(
redis_url: &str,
prefix: &str,
retry_config: RetryConfig,
) -> StateResult<Self> {
let client = Client::open(redis_url).map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Failed to create Redis client: {}", e),
})?;
let connection = client
.get_connection_manager()
.await
.map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Failed to connect to Redis: {}", e),
})?;
Ok(Self {
connection,
prefix: prefix.to_string(),
retry_config,
})
}
fn resource_key(&self, resource: &str) -> String {
format!("{}{}", self.prefix, resource)
}
fn calculate_delay(&self, attempt: u32) -> Duration {
let delay = self.retry_config.initial_delay.as_millis() as f64
* self.retry_config.backoff_multiplier.powi(attempt as i32);
Duration::from_millis(delay.min(self.retry_config.max_delay.as_millis() as f64) as u64)
}
}
#[async_trait]
impl DistributedLock for RedisDistributedLock {
async fn acquire(&self, resource: &str, ttl: Duration) -> StateResult<LockGuard> {
let key = self.resource_key(resource);
let token = LockToken::new();
let ttl_secs = ttl.as_secs();
debug!(
resource = %resource,
token = %token,
ttl_secs = ttl_secs,
"Attempting to acquire lock"
);
let mut attempt = 0;
loop {
attempt += 1;
let mut conn = self.connection.clone();
let result: Result<Value, RedisError> = redis::cmd("SET")
.arg(&key)
.arg(token.value())
.arg("NX")
.arg("EX")
.arg(ttl_secs)
.query_async(&mut conn)
.await;
match result {
Ok(Value::Okay) => {
debug!(
resource = %resource,
token = %token,
attempt = attempt,
"Lock acquired"
);
let guard = LockGuard::new(
resource.to_string(),
token,
Utc::now(),
ttl,
Arc::new(Self {
connection: self.connection.clone(),
prefix: self.prefix.clone(),
retry_config: self.retry_config.clone(),
}),
);
return Ok(guard);
}
Ok(_) => {
if attempt >= self.retry_config.max_attempts {
return Err(StateError::TransactionFailed {
operation: "acquire_lock".to_string(),
reason: format!(
"Failed to acquire lock after {} attempts",
attempt
),
});
}
let delay = self.calculate_delay(attempt);
debug!(
resource = %resource,
attempt = attempt,
delay_ms = delay.as_millis(),
"Lock held by another instance, retrying"
);
tokio::time::sleep(delay).await;
}
Err(e) => {
return Err(StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Lock acquisition failed: {}", e),
});
}
}
}
}
async fn try_acquire(&self, resource: &str, ttl: Duration) -> StateResult<Option<LockGuard>> {
let key = self.resource_key(resource);
let token = LockToken::new();
let ttl_secs = ttl.as_secs();
debug!(
resource = %resource,
token = %token,
ttl_secs = ttl_secs,
"Attempting non-blocking lock acquisition"
);
let mut conn = self.connection.clone();
let result: Result<Value, RedisError> = redis::cmd("SET")
.arg(&key)
.arg(token.value())
.arg("NX")
.arg("EX")
.arg(ttl_secs)
.query_async(&mut conn)
.await;
match result {
Ok(Value::Okay) => {
debug!(resource = %resource, token = %token, "Lock acquired");
let guard = LockGuard::new(
resource.to_string(),
token,
Utc::now(),
ttl,
Arc::new(Self {
connection: self.connection.clone(),
prefix: self.prefix.clone(),
retry_config: self.retry_config.clone(),
}),
);
Ok(Some(guard))
}
Ok(_) => {
debug!(resource = %resource, "Lock already held");
Ok(None)
}
Err(e) => Err(StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Lock acquisition failed: {}", e),
}),
}
}
async fn release_internal(&self, resource: &str, token: &LockToken) -> StateResult<()> {
let key = self.resource_key(resource);
debug!(resource = %resource, token = %token, "Releasing lock");
let script = Script::new(RELEASE_SCRIPT);
let mut conn = self.connection.clone();
let result: i32 = script
.key(&key)
.arg(token.value())
.invoke_async(&mut conn)
.await
.map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Lock release failed: {}", e),
})?;
if result == 0 {
warn!(
resource = %resource,
token = %token,
"Lock release failed: token mismatch or already released"
);
return Err(StateError::TransactionFailed {
operation: "release_lock".to_string(),
reason: "Token mismatch or lock already released".to_string(),
});
}
debug!(resource = %resource, "Lock released");
Ok(())
}
async fn is_locked(&self, resource: &str) -> StateResult<bool> {
let key = self.resource_key(resource);
let mut conn = self.connection.clone();
let exists: bool = conn.exists(&key).await.map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Failed to check lock existence: {}", e),
})?;
Ok(exists)
}
async fn extend(&self, guard: &mut LockGuard, ttl: Duration) -> StateResult<()> {
let key = self.resource_key(guard.resource());
let ttl_ms = ttl.as_millis() as i64;
debug!(
resource = %guard.resource(),
token = %guard.token(),
ttl_ms = ttl_ms,
"Extending lock"
);
let script = Script::new(EXTEND_SCRIPT);
let mut conn = self.connection.clone();
let result: i32 = script
.key(&key)
.arg(guard.token().value())
.arg(ttl_ms)
.invoke_async(&mut conn)
.await
.map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Lock extension failed: {}", e),
})?;
if result == 0 {
warn!(
resource = %guard.resource(),
token = %guard.token(),
"Lock extension failed: token mismatch or lock expired"
);
return Err(StateError::TransactionFailed {
operation: "extend_lock".to_string(),
reason: "Token mismatch or lock expired".to_string(),
});
}
debug!(resource = %guard.resource(), "Lock extended");
Ok(())
}
async fn get_lock_info(&self, resource: &str) -> StateResult<Option<LockToken>> {
let key = self.resource_key(resource);
let mut conn = self.connection.clone();
let value: Option<String> = conn.get(&key).await.map_err(|e| StateError::StorageError {
backend_type: "Redis".to_string(),
details: format!("Failed to get lock info: {}", e),
})?;
Ok(value.map(LockToken::from_string))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
async fn create_test_lock() -> Option<RedisDistributedLock> {
match RedisDistributedLock::with_config(
"redis://localhost:6379",
"test:lock:",
RetryConfig::no_retry(),
)
.await
{
Ok(lock) => Some(lock),
Err(_) => {
eprintln!("Skipping Redis tests - Redis not available");
None
}
}
}
#[tokio::test]
async fn test_acquire_and_release() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
assert!(!lock.is_locked(&resource).await.unwrap());
let guard = lock
.acquire(&resource, Duration::from_secs(10))
.await
.unwrap();
assert!(lock.is_locked(&resource).await.unwrap());
lock.release(guard).await.unwrap();
assert!(!lock.is_locked(&resource).await.unwrap());
}
#[tokio::test]
async fn test_try_acquire() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
let guard1 = lock
.try_acquire(&resource, Duration::from_secs(10))
.await
.unwrap();
assert!(guard1.is_some());
let guard2 = lock
.try_acquire(&resource, Duration::from_secs(10))
.await
.unwrap();
assert!(guard2.is_none());
drop(guard1);
tokio::time::sleep(Duration::from_millis(100)).await;
let guard3 = lock
.try_acquire(&resource, Duration::from_secs(10))
.await
.unwrap();
assert!(guard3.is_some());
}
#[tokio::test]
async fn test_lock_expiration() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
let guard = lock
.acquire(&resource, Duration::from_secs(1))
.await
.unwrap();
assert!(lock.is_locked(&resource).await.unwrap());
std::mem::forget(guard);
tokio::time::sleep(Duration::from_secs(2)).await;
assert!(!lock.is_locked(&resource).await.unwrap());
}
#[tokio::test]
async fn test_lock_extension() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
let mut guard = lock
.acquire(&resource, Duration::from_secs(2))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(1500)).await;
lock.extend(&mut guard, Duration::from_secs(5))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(1000)).await;
assert!(lock.is_locked(&resource).await.unwrap());
}
#[tokio::test]
async fn test_concurrent_access() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
let lock = Arc::new(lock);
let mut handles = vec![];
for i in 0..5 {
let lock = lock.clone();
let resource = resource.clone();
let handle = tokio::spawn(async move {
if let Some(guard) = lock
.try_acquire(&resource, Duration::from_secs(1))
.await
.unwrap()
{
tokio::time::sleep(Duration::from_millis(100)).await;
drop(guard);
Some(i)
} else {
None
}
});
handles.push(handle);
}
let mut successful = 0;
for handle in handles {
if handle.await.unwrap().is_some() {
successful += 1;
}
}
assert!(successful >= 1);
}
#[tokio::test]
async fn test_get_lock_info() {
let Some(lock) = create_test_lock().await else {
return;
};
let resource = format!("test:resource:{}", Uuid::new_v4());
assert!(lock.get_lock_info(&resource).await.unwrap().is_none());
let guard = lock
.acquire(&resource, Duration::from_secs(10))
.await
.unwrap();
let token = lock.get_lock_info(&resource).await.unwrap();
assert!(token.is_some());
assert_eq!(token.unwrap(), *guard.token());
}
#[test]
fn test_retry_config() {
let config = RetryConfig::default();
assert_eq!(config.max_attempts, 10);
let no_retry = RetryConfig::no_retry();
assert_eq!(no_retry.max_attempts, 1);
let aggressive = RetryConfig::aggressive();
assert_eq!(aggressive.max_attempts, 50);
}
}