armature_cache/
redis_cache.rs1use 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, aio::ConnectionManagerConfig};
9use std::future::Future;
10use std::time::Duration;
11
12#[derive(Clone)]
14pub struct RedisCache {
15 connection: ConnectionManager,
16 config: CacheConfig,
17}
18
19impl RedisCache {
20 pub async fn new(config: CacheConfig) -> CacheResult<Self> {
39 debug!("Connecting to Redis cache: {}", config.url);
40 let client =
41 Client::open(config.url.as_str()).map_err(|e| CacheError::Connection(e.to_string()))?;
42
43 let mut manager_config =
56 ConnectionManagerConfig::new().set_connection_timeout(Some(config.connection_timeout));
57 if config.max_connections > 0 {
58 manager_config = manager_config.set_concurrency_limit(config.max_connections);
59 }
60
61 let connection = ConnectionManager::new_with_config(client, manager_config)
62 .await
63 .map_err(|e| CacheError::Connection(e.to_string()))?;
64
65 debug!("Redis cache connection established");
66 Ok(Self { connection, config })
67 }
68
69 pub fn connection(&self) -> &ConnectionManager {
71 &self.connection
72 }
73
74 fn build_key(&self, key: &str) -> String {
76 self.config.build_key(key)
77 }
78
79 async fn with_op_timeout<F, T>(&self, fut: F) -> CacheResult<T>
86 where
87 F: Future<Output = redis::RedisResult<T>>,
88 {
89 match tokio::time::timeout(self.config.operation_timeout, fut).await {
90 Ok(result) => Ok(result?),
91 Err(_) => Err(CacheError::Timeout),
92 }
93 }
94
95 async fn scan_and_unlink(
103 conn: &mut ConnectionManager,
104 pattern: String,
105 ) -> redis::RedisResult<()> {
106 use futures::StreamExt;
107
108 const UNLINK_BATCH_SIZE: usize = 500;
110
111 let mut matched: Vec<String> = Vec::new();
112 {
113 let mut iter: redis::AsyncIter<'_, String> = conn.scan_match(pattern.as_str()).await?;
114 while let Some(key) = iter.next().await {
115 matched.push(key?);
116 }
117 }
118
119 for chunk in matched.chunks(UNLINK_BATCH_SIZE) {
120 let _: () = redis::cmd("UNLINK").arg(chunk).query_async(conn).await?;
121 }
122
123 Ok(())
124 }
125}
126
127#[async_trait]
128impl CacheStore for RedisCache {
129 async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
130 let key = self.build_key(key);
131 trace!("Cache GET: {}", key);
132 let mut conn = self.connection.clone();
133
134 let value: Option<String> = self.with_op_timeout(conn.get(&key)).await?;
135 trace!(
136 "Cache {} for: {}",
137 if value.is_some() { "HIT" } else { "MISS" },
138 key
139 );
140 Ok(value)
141 }
142
143 async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
144 let key = self.build_key(key);
145 trace!("Cache SET: {} (ttl: {:?})", key, ttl);
146 let mut conn = self.connection.clone();
147
148 let ttl = ttl.or(self.config.default_ttl);
149
150 if let Some(ttl) = ttl {
151 let ttl_seconds = ttl.as_secs();
152 let _: () = self
153 .with_op_timeout(conn.set_ex(&key, value, ttl_seconds))
154 .await?;
155 } else {
156 let _: () = self.with_op_timeout(conn.set(&key, value)).await?;
157 }
158
159 Ok(())
160 }
161
162 async fn delete(&self, key: &str) -> CacheResult<()> {
163 let key = self.build_key(key);
164 let mut conn = self.connection.clone();
165 let _: () = self.with_op_timeout(conn.del(&key)).await?;
166 Ok(())
167 }
168
169 async fn exists(&self, key: &str) -> CacheResult<bool> {
170 let key = self.build_key(key);
171 let mut conn = self.connection.clone();
172 let exists: bool = self.with_op_timeout(conn.exists(&key)).await?;
173 Ok(exists)
174 }
175
176 async fn clear(&self) -> CacheResult<()> {
192 match self.config.key_prefix.as_deref() {
193 Some(prefix) if !prefix.is_empty() => {
194 let pattern = format!("{prefix}:*");
195 let mut conn = self.connection.clone();
196 self.with_op_timeout(Self::scan_and_unlink(&mut conn, pattern))
197 .await?;
198 }
199 _ => {
200 let mut conn = self.connection.clone();
201 let _: () = self
202 .with_op_timeout(redis::cmd("FLUSHDB").query_async(&mut conn))
203 .await?;
204 }
205 }
206 Ok(())
207 }
208
209 async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
210 let key = self.build_key(key);
211 let mut conn = self.connection.clone();
212
213 let ttl_seconds: i64 = self.with_op_timeout(conn.ttl(&key)).await?;
214
215 match ttl_seconds {
216 -2 => Ok(None), -1 => Ok(None), seconds if seconds > 0 => Ok(Some(Duration::from_secs(seconds as u64))),
219 _ => Ok(None),
220 }
221 }
222
223 async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
224 let key = self.build_key(key);
225 let mut conn = self.connection.clone();
226 let ttl_seconds = ttl.as_secs();
227 let _: () = self
228 .with_op_timeout(conn.expire(&key, ttl_seconds as i64))
229 .await?;
230 Ok(())
231 }
232
233 async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
234 let key = self.build_key(key);
235 let mut conn = self.connection.clone();
236 let new_value: i64 = self.with_op_timeout(conn.incr(&key, delta)).await?;
237 Ok(new_value)
238 }
239
240 async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
241 let key = self.build_key(key);
242 let mut conn = self.connection.clone();
243 let new_value: i64 = self.with_op_timeout(conn.decr(&key, delta)).await?;
244 Ok(new_value)
245 }
246
247 async fn mget(&self, keys: &[&str]) -> CacheResult<Vec<Option<String>>> {
252 if keys.is_empty() {
253 return Ok(Vec::new());
254 }
255 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
256 trace!("Cache MGET: {} keys", full_keys.len());
257 let mut conn = self.connection.clone();
258 let values: Vec<Option<String>> = self
259 .with_op_timeout(redis::cmd("MGET").arg(&full_keys).query_async(&mut conn))
260 .await?;
261 Ok(values)
262 }
263
264 async fn mset(&self, items: &[(&str, String)], ttl: Option<Duration>) -> CacheResult<()> {
271 if items.is_empty() {
272 return Ok(());
273 }
274 let mut conn = self.connection.clone();
275 let ttl = ttl.or(self.config.default_ttl);
276
277 if let Some(ttl) = ttl {
278 let ttl_seconds = ttl.as_secs();
279 trace!("Cache MSET (pipelined SET EX): {} items", items.len());
280 let mut pipe = redis::pipe();
281 for (key, value) in items {
282 pipe.cmd("SET")
283 .arg(self.build_key(key))
284 .arg(value)
285 .arg("EX")
286 .arg(ttl_seconds)
287 .ignore();
288 }
289 let _: () = self.with_op_timeout(pipe.query_async(&mut conn)).await?;
290 } else {
291 trace!("Cache MSET: {} items", items.len());
292 let mut cmd = redis::cmd("MSET");
293 for (key, value) in items {
294 cmd.arg(self.build_key(key)).arg(value);
295 }
296 let _: () = self.with_op_timeout(cmd.query_async(&mut conn)).await?;
297 }
298 Ok(())
299 }
300
301 async fn mdel(&self, keys: &[&str]) -> CacheResult<()> {
303 if keys.is_empty() {
304 return Ok(());
305 }
306 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
307 trace!("Cache DEL: {} keys", full_keys.len());
308 let mut conn = self.connection.clone();
309 let _: () = self
310 .with_op_timeout(redis::cmd("DEL").arg(&full_keys).query_async(&mut conn))
311 .await?;
312 Ok(())
313 }
314
315 fn supports_atomic_sets(&self) -> bool {
318 true
319 }
320
321 async fn set_add(&self, set_key: &str, member: &str) -> CacheResult<()> {
326 let set_key = self.build_key(set_key);
327 let mut conn = self.connection.clone();
328 let _: () = self.with_op_timeout(conn.sadd(&set_key, member)).await?;
329 Ok(())
330 }
331
332 async fn set_remove(&self, set_key: &str, member: &str) -> CacheResult<()> {
334 let set_key = self.build_key(set_key);
335 let mut conn = self.connection.clone();
336 let _: () = self.with_op_timeout(conn.srem(&set_key, member)).await?;
337 Ok(())
338 }
339
340 async fn set_members(&self, set_key: &str) -> CacheResult<Vec<String>> {
342 let set_key = self.build_key(set_key);
343 let mut conn = self.connection.clone();
344 let members: Vec<String> = self.with_op_timeout(conn.smembers(&set_key)).await?;
345 Ok(members)
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_build_key() {
355 let config = CacheConfig::redis("redis://localhost:6379")
356 .unwrap()
357 .with_key_prefix("test");
358
359 assert_eq!(config.build_key("key"), "test:key");
362 }
363}