armature_cache/
redis_cache.rs

1//! Redis cache implementation.
2
3use crate::config::CacheConfig;
4use crate::error::{CacheError, CacheResult};
5use crate::traits::CacheStore;
6use armature_log::{debug, trace};
7use async_trait::async_trait;
8use redis::{AsyncCommands, Client, aio::ConnectionManager};
9use std::time::Duration;
10
11/// Redis cache store.
12#[derive(Clone)]
13pub struct RedisCache {
14    connection: ConnectionManager,
15    config: CacheConfig,
16}
17
18impl RedisCache {
19    /// Create a new Redis cache instance.
20    ///
21    /// # Arguments
22    ///
23    /// * `config` - Cache configuration
24    ///
25    /// # Examples
26    ///
27    /// ```no_run
28    /// use armature_cache::*;
29    ///
30    /// #[tokio::main]
31    /// async fn main() -> Result<(), CacheError> {
32    ///     let config = CacheConfig::redis("redis://localhost:6379")?;
33    ///     let cache = RedisCache::new(config).await?;
34    ///     Ok(())
35    /// }
36    /// ```
37    pub async fn new(config: CacheConfig) -> CacheResult<Self> {
38        debug!("Connecting to Redis cache: {}", config.url);
39        let client =
40            Client::open(config.url.as_str()).map_err(|e| CacheError::Connection(e.to_string()))?;
41
42        let connection = ConnectionManager::new(client)
43            .await
44            .map_err(|e| CacheError::Connection(e.to_string()))?;
45
46        debug!("Redis cache connection established");
47        Ok(Self { connection, config })
48    }
49
50    /// Get the underlying connection manager.
51    pub fn connection(&self) -> &ConnectionManager {
52        &self.connection
53    }
54
55    /// Build the full key with prefix.
56    fn build_key(&self, key: &str) -> String {
57        self.config.build_key(key)
58    }
59}
60
61#[async_trait]
62impl CacheStore for RedisCache {
63    async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
64        let key = self.build_key(key);
65        trace!("Cache GET: {}", key);
66        let mut conn = self.connection.clone();
67
68        let value: Option<String> = conn.get(&key).await?;
69        trace!(
70            "Cache {} for: {}",
71            if value.is_some() { "HIT" } else { "MISS" },
72            key
73        );
74        Ok(value)
75    }
76
77    async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
78        let key = self.build_key(key);
79        trace!("Cache SET: {} (ttl: {:?})", key, ttl);
80        let mut conn = self.connection.clone();
81
82        let ttl = ttl.or(self.config.default_ttl);
83
84        if let Some(ttl) = ttl {
85            let ttl_seconds = ttl.as_secs();
86            let _: () = conn.set_ex(&key, value, ttl_seconds).await?;
87        } else {
88            let _: () = conn.set(&key, value).await?;
89        }
90
91        Ok(())
92    }
93
94    async fn delete(&self, key: &str) -> CacheResult<()> {
95        let key = self.build_key(key);
96        let mut conn = self.connection.clone();
97        let _: () = conn.del(&key).await?;
98        Ok(())
99    }
100
101    async fn exists(&self, key: &str) -> CacheResult<bool> {
102        let key = self.build_key(key);
103        let mut conn = self.connection.clone();
104        let exists: bool = conn.exists(&key).await?;
105        Ok(exists)
106    }
107
108    async fn clear(&self) -> CacheResult<()> {
109        let mut conn = self.connection.clone();
110        let _: () = redis::cmd("FLUSHDB").query_async(&mut conn).await?;
111        Ok(())
112    }
113
114    async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
115        let key = self.build_key(key);
116        let mut conn = self.connection.clone();
117
118        let ttl_seconds: i64 = conn.ttl(&key).await?;
119
120        match ttl_seconds {
121            -2 => Ok(None), // Key doesn't exist
122            -1 => Ok(None), // Key has no expiration
123            seconds if seconds > 0 => Ok(Some(Duration::from_secs(seconds as u64))),
124            _ => Ok(None),
125        }
126    }
127
128    async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
129        let key = self.build_key(key);
130        let mut conn = self.connection.clone();
131        let ttl_seconds = ttl.as_secs();
132        let _: () = conn.expire(&key, ttl_seconds as i64).await?;
133        Ok(())
134    }
135
136    async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
137        let key = self.build_key(key);
138        let mut conn = self.connection.clone();
139        let new_value: i64 = conn.incr(&key, delta).await?;
140        Ok(new_value)
141    }
142
143    async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
144        let key = self.build_key(key);
145        let mut conn = self.connection.clone();
146        let new_value: i64 = conn.decr(&key, delta).await?;
147        Ok(new_value)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_build_key() {
157        let config = CacheConfig::redis("redis://localhost:6379")
158            .unwrap()
159            .with_key_prefix("test");
160
161        // Note: Can't easily test async without a real Redis instance
162        // This is just to verify the struct can be created
163        assert_eq!(config.build_key("key"), "test:key");
164    }
165}