use anyhow::{Context, Result};
use async_trait::async_trait;
use redis::AsyncCommands;
use uuid::Uuid;
use super::cache::{CachedOverlayEntry, WorkspaceCache, WorkspaceSnapshot};
pub struct ValkeyCache {
conn: redis::aio::ConnectionManager,
ttl_secs: u32,
}
impl ValkeyCache {
pub async fn new(url: &str, ttl_secs: u32) -> Result<Self, redis::RedisError> {
let client = redis::Client::open(url)?;
let conn = redis::aio::ConnectionManager::new(client).await?;
Ok(Self { conn, ttl_secs })
}
fn meta_key(id: &Uuid) -> String {
format!("{{{id}}}:meta")
}
fn graph_key(id: &Uuid) -> String {
format!("{{{id}}}:graph")
}
fn file_key(id: &Uuid, path: &str) -> String {
format!("{{{id}}}:file:{path}")
}
fn files_set_key(id: &Uuid) -> String {
format!("{{{id}}}:files")
}
}
#[async_trait]
impl WorkspaceCache for ValkeyCache {
async fn cache_workspace(&self, id: &Uuid, snapshot: &WorkspaceSnapshot) -> Result<()> {
let key = Self::meta_key(id);
let bytes =
rmp_serde::to_vec_named(snapshot).context("ValkeyCache: failed to serialize snapshot")?;
let mut conn = self.conn.clone();
conn.set_ex::<_, _, ()>(&key, bytes.as_slice(), u64::from(self.ttl_secs))
.await
.context("ValkeyCache: SET meta failed")?;
Ok(())
}
async fn get_workspace(&self, id: &Uuid) -> Result<Option<WorkspaceSnapshot>> {
let key = Self::meta_key(id);
let mut conn = self.conn.clone();
let bytes: Option<Vec<u8>> = conn
.get(&key)
.await
.context("ValkeyCache: GET meta failed")?;
match bytes {
Some(b) => {
let snap = rmp_serde::from_slice(&b)
.context("ValkeyCache: failed to deserialize snapshot")?;
Ok(Some(snap))
}
None => Ok(None),
}
}
async fn cache_file(
&self,
workspace_id: &Uuid,
path: &str,
entry: &CachedOverlayEntry,
) -> Result<()> {
let file_key = Self::file_key(workspace_id, path);
let set_key = Self::files_set_key(workspace_id);
let bytes =
rmp_serde::to_vec_named(entry).context("ValkeyCache: failed to serialize overlay entry")?;
let mut conn = self.conn.clone();
redis::pipe()
.atomic()
.set_ex(&file_key, bytes.as_slice(), u64::from(self.ttl_secs))
.sadd(&set_key, path)
.expire(&set_key, i64::from(self.ttl_secs))
.query_async::<()>(&mut conn)
.await
.context("ValkeyCache: pipeline SET+SADD failed")?;
Ok(())
}
async fn get_file(
&self,
workspace_id: &Uuid,
path: &str,
) -> Result<Option<CachedOverlayEntry>> {
let key = Self::file_key(workspace_id, path);
let mut conn = self.conn.clone();
let bytes: Option<Vec<u8>> = conn
.get(&key)
.await
.context("ValkeyCache: GET file failed")?;
match bytes {
Some(b) => {
let entry = rmp_serde::from_slice(&b)
.context("ValkeyCache: failed to deserialize overlay entry")?;
Ok(Some(entry))
}
None => Ok(None),
}
}
async fn list_files(&self, workspace_id: &Uuid) -> Result<Vec<String>> {
let key = Self::files_set_key(workspace_id);
let mut conn = self.conn.clone();
let paths: Vec<String> = conn
.smembers(&key)
.await
.context("ValkeyCache: SMEMBERS files failed")?;
Ok(paths)
}
async fn cache_graph(&self, workspace_id: &Uuid, graph_data: &[u8]) -> Result<()> {
let key = Self::graph_key(workspace_id);
let mut conn = self.conn.clone();
conn.set_ex::<_, _, ()>(&key, graph_data, u64::from(self.ttl_secs))
.await
.context("ValkeyCache: SET graph failed")?;
Ok(())
}
async fn get_graph(&self, workspace_id: &Uuid) -> Result<Option<Vec<u8>>> {
let key = Self::graph_key(workspace_id);
let mut conn = self.conn.clone();
let bytes: Option<Vec<u8>> = conn
.get(&key)
.await
.context("ValkeyCache: GET graph failed")?;
Ok(bytes)
}
async fn evict(&self, id: &Uuid) -> Result<()> {
let meta_key = Self::meta_key(id);
let graph_key = Self::graph_key(id);
let set_key = Self::files_set_key(id);
let mut conn = self.conn.clone();
let file_paths: Vec<String> = conn
.smembers(&set_key)
.await
.context("ValkeyCache: SMEMBERS during evict failed")?;
let mut pipe = redis::pipe();
pipe.atomic();
pipe.del(&meta_key);
pipe.del(&graph_key);
pipe.del(&set_key);
for path in &file_paths {
pipe.del(Self::file_key(id, path));
}
pipe.query_async::<()>(&mut conn)
.await
.context("ValkeyCache: pipeline DEL during evict failed")?;
Ok(())
}
async fn touch(&self, id: &Uuid) -> Result<()> {
let meta_key = Self::meta_key(id);
let graph_key = Self::graph_key(id);
let set_key = Self::files_set_key(id);
let mut conn = self.conn.clone();
let file_paths: Vec<String> = conn
.smembers(&set_key)
.await
.context("ValkeyCache: SMEMBERS during touch failed")?;
let ttl = i64::from(self.ttl_secs);
let mut pipe = redis::pipe();
pipe.atomic();
pipe.expire(&meta_key, ttl).ignore();
pipe.expire(&graph_key, ttl).ignore();
pipe.expire(&set_key, ttl).ignore();
for path in &file_paths {
pipe.expire(Self::file_key(id, path), ttl).ignore();
}
pipe.query_async::<()>(&mut conn)
.await
.context("ValkeyCache: pipeline EXPIRE during touch failed")?;
Ok(())
}
}