use std::sync::OnceLock;
use async_trait::async_trait;
use redis::aio::ConnectionManager;
use redis::{AsyncCommands, Client, IntoConnectionInfo, RedisResult, Script};
use tokio::sync::OnceCell;
use crate::types::Backend;
use crate::Error;
#[derive(Clone)]
pub struct Redis {
client: Client,
conn: OnceCell<ConnectionManager>,
}
impl std::fmt::Debug for Redis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Redis")
.field("connection_info", &self.client.get_connection_info().addr)
.finish()
}
}
impl Redis {
pub fn new<T: IntoConnectionInfo>(connection_params: T) -> Self {
let client = Client::open(connection_params).expect("Failed to create Redis client");
Self::new_with_client(client)
}
pub fn new_with_client(client: Client) -> Self {
Self {
client,
conn: OnceCell::new(),
}
}
async fn conn(&self) -> Result<ConnectionManager, Error> {
self.conn
.get_or_try_init(|| ConnectionManager::new(self.client.clone()))
.await
.cloned()
.map_err(Into::into)
}
fn key_waiting(&self, queue: &str) -> String {
format!("{}:waiting", queue)
}
fn key_delayed(&self, queue: &str) -> String {
format!("{}:delayed", queue)
}
fn key_active(&self, queue: &str) -> String {
format!("{}:active", queue)
}
fn key_storage(&self, queue: &str) -> String {
format!("{}:storage", queue)
}
fn key_lock(&self, job_id: &str) -> String {
format!("aj:lock:{}", job_id)
}
}
const LUA_DELAYED_MOVE_READY: &str = r#"
local delayed_key = KEYS[1]
local waiting_key = KEYS[2]
local now_ms = tonumber(ARGV[1])
local ready = redis.call('ZRANGEBYSCORE', delayed_key, '-inf', now_ms)
local count = 0
for i, job_id in ipairs(ready) do
redis.call('ZREM', delayed_key, job_id)
redis.call('RPUSH', waiting_key, job_id)
count = count + 1
end
return count
"#;
const LUA_CLAIM_JOB: &str = r#"
local waiting_key = KEYS[1]
local active_key = KEYS[2]
local worker_id = ARGV[1]
local lock_ttl = tonumber(ARGV[2])
local job_id = redis.call('LPOP', waiting_key)
if not job_id then
return nil
end
local lock_key = 'aj:lock:' .. job_id
local acquired = redis.call('SET', lock_key, worker_id, 'NX', 'PX', lock_ttl)
if acquired then
redis.call('RPUSH', active_key, job_id)
return job_id
else
-- Failed to acquire lock, put job back
redis.call('LPUSH', waiting_key, job_id)
return nil
end
"#;
const LUA_LOCK_RELEASE: &str = r#"
local lock_key = KEYS[1]
local worker_id = ARGV[1]
if redis.call('GET', lock_key) == worker_id then
return redis.call('DEL', lock_key)
end
return 0
"#;
const LUA_LOCK_EXTEND: &str = r#"
local lock_key = KEYS[1]
local worker_id = ARGV[1]
local ttl_ms = tonumber(ARGV[2])
if redis.call('GET', lock_key) == worker_id then
return redis.call('PEXPIRE', lock_key, ttl_ms)
end
return 0
"#;
const LUA_REQUEUE_ORPHANED: &str = r#"
local active_key = KEYS[1]
local waiting_key = KEYS[2]
local orphaned = {}
local job_ids = redis.call('LRANGE', active_key, 0, -1)
for i, job_id in ipairs(job_ids) do
local lock_key = 'aj:lock:' .. job_id
if redis.call('EXISTS', lock_key) == 0 then
redis.call('LREM', active_key, 1, job_id)
redis.call('RPUSH', waiting_key, job_id)
table.insert(orphaned, job_id)
end
end
return orphaned
"#;
fn script(cell: &'static OnceLock<Script>, body: &'static str) -> &'static Script {
cell.get_or_init(|| Script::new(body))
}
fn lua_delayed_move_ready() -> &'static Script {
static S: OnceLock<Script> = OnceLock::new();
script(&S, LUA_DELAYED_MOVE_READY)
}
fn lua_claim_job() -> &'static Script {
static S: OnceLock<Script> = OnceLock::new();
script(&S, LUA_CLAIM_JOB)
}
fn lua_lock_release() -> &'static Script {
static S: OnceLock<Script> = OnceLock::new();
script(&S, LUA_LOCK_RELEASE)
}
fn lua_lock_extend() -> &'static Script {
static S: OnceLock<Script> = OnceLock::new();
script(&S, LUA_LOCK_EXTEND)
}
fn lua_requeue_orphaned() -> &'static Script {
static S: OnceLock<Script> = OnceLock::new();
script(&S, LUA_REQUEUE_ORPHANED)
}
#[async_trait]
impl Backend for Redis {
async fn waiting_push(&self, queue: &str, job_id: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_waiting(queue);
conn.rpush::<_, _, ()>(&key, job_id).await?;
Ok(())
}
async fn waiting_pop(&self, queue: &str) -> Result<Option<String>, Error> {
let mut conn = self.conn().await?;
let key = self.key_waiting(queue);
let result: Option<String> = conn.lpop(&key, None).await?;
Ok(result)
}
async fn waiting_len(&self, queue: &str) -> Result<usize, Error> {
let mut conn = self.conn().await?;
let key = self.key_waiting(queue);
let len: usize = conn.llen(&key).await?;
Ok(len)
}
async fn delayed_push(&self, queue: &str, job_id: &str, run_at_ms: i64) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_delayed(queue);
conn.zadd::<_, _, _, ()>(&key, job_id, run_at_ms).await?;
Ok(())
}
async fn delayed_move_ready(&self, queue: &str, now_ms: i64) -> Result<usize, Error> {
let mut conn = self.conn().await?;
let delayed_key = self.key_delayed(queue);
let waiting_key = self.key_waiting(queue);
let script = lua_delayed_move_ready();
let count: usize = script
.key(&delayed_key)
.key(&waiting_key)
.arg(now_ms)
.invoke_async(&mut conn)
.await?;
Ok(count)
}
async fn delayed_remove(&self, queue: &str, job_id: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_delayed(queue);
conn.zrem::<_, _, ()>(&key, job_id).await?;
Ok(())
}
async fn delayed_len(&self, queue: &str) -> Result<usize, Error> {
let mut conn = self.conn().await?;
let key = self.key_delayed(queue);
let len: usize = conn.zcard(&key).await?;
Ok(len)
}
async fn active_push(&self, queue: &str, job_id: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_active(queue);
conn.rpush::<_, _, ()>(&key, job_id).await?;
Ok(())
}
async fn active_remove(&self, queue: &str, job_id: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_active(queue);
conn.lrem::<_, _, ()>(&key, 1, job_id).await?;
Ok(())
}
async fn active_len(&self, queue: &str) -> Result<usize, Error> {
let mut conn = self.conn().await?;
let key = self.key_active(queue);
let len: usize = conn.llen(&key).await?;
Ok(len)
}
async fn active_list(&self, queue: &str) -> Result<Vec<String>, Error> {
let mut conn = self.conn().await?;
let key = self.key_active(queue);
let jobs: Vec<String> = conn.lrange(&key, 0, -1).await?;
Ok(jobs)
}
async fn job_save(&self, queue: &str, job_id: &str, data: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_storage(queue);
conn.hset::<_, _, _, ()>(&key, job_id, data).await?;
Ok(())
}
async fn job_get(&self, queue: &str, job_id: &str) -> Result<Option<String>, Error> {
let mut conn = self.conn().await?;
let key = self.key_storage(queue);
let data: Option<String> = conn.hget(&key, job_id).await?;
Ok(data)
}
async fn job_delete(&self, queue: &str, job_id: &str) -> Result<(), Error> {
let mut conn = self.conn().await?;
let key = self.key_storage(queue);
conn.hdel::<_, _, ()>(&key, job_id).await?;
Ok(())
}
async fn lock_acquire(
&self,
job_id: &str,
worker_id: &str,
ttl_ms: u64,
) -> Result<bool, Error> {
let mut conn = self.conn().await?;
let lock_key = self.key_lock(job_id);
let result: RedisResult<Option<String>> = redis::cmd("SET")
.arg(&lock_key)
.arg(worker_id)
.arg("NX")
.arg("PX")
.arg(ttl_ms)
.query_async(&mut conn)
.await;
match result {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(e) => Err(e.into()),
}
}
async fn lock_release(&self, job_id: &str, worker_id: &str) -> Result<bool, Error> {
let mut conn = self.conn().await?;
let lock_key = self.key_lock(job_id);
let script = lua_lock_release();
let result: i32 = script
.key(&lock_key)
.arg(worker_id)
.invoke_async(&mut conn)
.await?;
Ok(result == 1)
}
async fn lock_extend(&self, job_id: &str, worker_id: &str, ttl_ms: u64) -> Result<bool, Error> {
let mut conn = self.conn().await?;
let lock_key = self.key_lock(job_id);
let script = lua_lock_extend();
let result: i32 = script
.key(&lock_key)
.arg(worker_id)
.arg(ttl_ms)
.invoke_async(&mut conn)
.await?;
Ok(result == 1)
}
async fn claim_job(
&self,
queue: &str,
worker_id: &str,
lock_ttl_ms: u64,
) -> Result<Option<String>, Error> {
let mut conn = self.conn().await?;
let waiting_key = self.key_waiting(queue);
let active_key = self.key_active(queue);
let script = lua_claim_job();
let result: Option<String> = script
.key(&waiting_key)
.key(&active_key)
.arg(worker_id)
.arg(lock_ttl_ms)
.invoke_async(&mut conn)
.await?;
Ok(result)
}
async fn complete_job(
&self,
queue: &str,
job_id: &str,
worker_id: &str,
) -> Result<bool, Error> {
self.active_remove(queue, job_id).await?;
self.lock_release(job_id, worker_id).await?;
Ok(true)
}
async fn fail_job(&self, queue: &str, job_id: &str, worker_id: &str) -> Result<bool, Error> {
self.active_remove(queue, job_id).await?;
self.lock_release(job_id, worker_id).await?;
Ok(true)
}
async fn requeue_orphaned(&self, queue: &str) -> Result<Vec<String>, Error> {
let mut conn = self.conn().await?;
let active_key = self.key_active(queue);
let waiting_key = self.key_waiting(queue);
let script = lua_requeue_orphaned();
let orphaned: Vec<String> = script
.key(&active_key)
.key(&waiting_key)
.invoke_async(&mut conn)
.await?;
Ok(orphaned)
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn test_redis() -> Redis {
Redis::new("redis://localhost:6379/")
}
fn unique_queue() -> String {
format!("test:{}", Uuid::new_v4())
}
fn unique_job() -> String {
format!("job:{}", Uuid::new_v4())
}
async fn cleanup(redis: &Redis, queue: &str, job_ids: &[&str]) {
let mut conn = redis.conn().await.unwrap();
let mut cmd = redis::cmd("DEL");
cmd.arg(redis.key_waiting(queue))
.arg(redis.key_delayed(queue))
.arg(redis.key_active(queue))
.arg(redis.key_storage(queue));
for job_id in job_ids {
cmd.arg(redis.key_lock(job_id));
}
let _: () = cmd.query_async(&mut conn).await.unwrap();
}
#[tokio::test]
async fn test_waiting_queue() {
let redis = test_redis();
let queue = unique_queue();
let job1 = unique_job();
let job2 = unique_job();
redis.waiting_push(&queue, &job1).await.unwrap();
redis.waiting_push(&queue, &job2).await.unwrap();
assert_eq!(redis.waiting_len(&queue).await.unwrap(), 2);
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), Some(job1.clone()));
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), Some(job2.clone()));
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), None);
cleanup(&redis, &queue, &[&job1, &job2]).await;
}
#[tokio::test]
async fn test_delayed_queue() {
let redis = test_redis();
let queue = unique_queue();
let job1 = unique_job();
let job2 = unique_job();
let job3 = unique_job();
redis.delayed_push(&queue, &job1, 1000).await.unwrap();
redis.delayed_push(&queue, &job2, 2000).await.unwrap();
redis.delayed_push(&queue, &job3, 3000).await.unwrap();
assert_eq!(redis.delayed_len(&queue).await.unwrap(), 3);
let moved = redis.delayed_move_ready(&queue, 2500).await.unwrap();
assert_eq!(moved, 2);
assert_eq!(redis.delayed_len(&queue).await.unwrap(), 1);
assert_eq!(redis.waiting_len(&queue).await.unwrap(), 2);
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), Some(job1.clone()));
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), Some(job2.clone()));
cleanup(&redis, &queue, &[&job1, &job2, &job3]).await;
}
#[tokio::test]
async fn test_claim_job() {
let redis = test_redis();
let queue = unique_queue();
let job1 = unique_job();
let job2 = unique_job();
redis.waiting_push(&queue, &job1).await.unwrap();
redis.waiting_push(&queue, &job2).await.unwrap();
let job = redis.claim_job(&queue, "worker1", 30000).await.unwrap();
assert_eq!(job, Some(job1.clone()));
assert_eq!(redis.waiting_len(&queue).await.unwrap(), 1);
assert_eq!(redis.active_len(&queue).await.unwrap(), 1);
let mut conn = redis.conn().await.unwrap();
let lock_value: Option<String> = conn.get(redis.key_lock(&job1)).await.unwrap();
assert_eq!(lock_value, Some("worker1".to_string()));
cleanup(&redis, &queue, &[&job1, &job2]).await;
}
#[tokio::test]
async fn test_lock_operations() {
let redis = test_redis();
let job_id = unique_job();
assert!(redis.lock_acquire(&job_id, "worker1", 30000).await.unwrap());
assert!(!redis.lock_acquire(&job_id, "worker2", 30000).await.unwrap());
assert!(redis.lock_extend(&job_id, "worker1", 60000).await.unwrap());
assert!(!redis.lock_extend(&job_id, "worker2", 60000).await.unwrap());
assert!(!redis.lock_release(&job_id, "worker2").await.unwrap());
assert!(redis.lock_release(&job_id, "worker1").await.unwrap());
assert!(redis.lock_acquire(&job_id, "worker2", 30000).await.unwrap());
redis.lock_release(&job_id, "worker2").await.unwrap();
}
#[tokio::test]
async fn test_requeue_orphaned() {
let redis = test_redis();
let queue = unique_queue();
let job1 = unique_job();
let job2 = unique_job();
redis.active_push(&queue, &job1).await.unwrap();
redis.active_push(&queue, &job2).await.unwrap();
assert!(redis.lock_acquire(&job1, "worker1", 30000).await.unwrap());
let orphaned = redis.requeue_orphaned(&queue).await.unwrap();
assert_eq!(orphaned, vec![job2.clone()]);
assert_eq!(redis.active_len(&queue).await.unwrap(), 1);
assert_eq!(redis.waiting_len(&queue).await.unwrap(), 1);
assert_eq!(redis.waiting_pop(&queue).await.unwrap(), Some(job2.clone()));
cleanup(&redis, &queue, &[&job1, &job2]).await;
}
#[tokio::test]
async fn test_job_storage() {
let redis = test_redis();
let queue = unique_queue();
let job1 = unique_job();
redis
.job_save(&queue, &job1, r#"{"data": 1}"#)
.await
.unwrap();
let data = redis.job_get(&queue, &job1).await.unwrap();
assert_eq!(data, Some(r#"{"data": 1}"#.to_string()));
redis.job_delete(&queue, &job1).await.unwrap();
assert_eq!(redis.job_get(&queue, &job1).await.unwrap(), None);
cleanup(&redis, &queue, &[&job1]).await;
}
}