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 set_json_forever(&self, key: &str, value: String) -> CacheResult<()> {
165 let key = self.build_key(key);
166 trace!("Cache SET (no expiry): {}", key);
167 let mut conn = self.connection.clone();
168 let _: () = self.with_op_timeout(conn.set(&key, value)).await?;
169 Ok(())
170 }
171
172 async fn delete(&self, key: &str) -> CacheResult<()> {
173 let key = self.build_key(key);
174 let mut conn = self.connection.clone();
175 let _: () = self.with_op_timeout(conn.del(&key)).await?;
176 Ok(())
177 }
178
179 async fn exists(&self, key: &str) -> CacheResult<bool> {
180 let key = self.build_key(key);
181 let mut conn = self.connection.clone();
182 let exists: bool = self.with_op_timeout(conn.exists(&key)).await?;
183 Ok(exists)
184 }
185
186 async fn clear(&self) -> CacheResult<()> {
202 match self.config.key_prefix.as_deref() {
203 Some(prefix) if !prefix.is_empty() => {
204 let pattern = format!("{prefix}:*");
205 let mut conn = self.connection.clone();
206 self.with_op_timeout(Self::scan_and_unlink(&mut conn, pattern))
207 .await?;
208 }
209 _ => {
210 let mut conn = self.connection.clone();
211 let _: () = self
212 .with_op_timeout(redis::cmd("FLUSHDB").query_async(&mut conn))
213 .await?;
214 }
215 }
216 Ok(())
217 }
218
219 async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
220 let key = self.build_key(key);
221 let mut conn = self.connection.clone();
222
223 let ttl_seconds: i64 = self.with_op_timeout(conn.ttl(&key)).await?;
224
225 match ttl_seconds {
226 -2 => Ok(None), -1 => Ok(None), seconds if seconds > 0 => Ok(Some(Duration::from_secs(seconds as u64))),
229 _ => Ok(None),
230 }
231 }
232
233 async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
234 let key = self.build_key(key);
235 let mut conn = self.connection.clone();
236 let ttl_seconds = ttl.as_secs();
237 let _: () = self
238 .with_op_timeout(conn.expire(&key, ttl_seconds as i64))
239 .await?;
240 Ok(())
241 }
242
243 async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
244 let key = self.build_key(key);
245 let mut conn = self.connection.clone();
246 let new_value: i64 = self.with_op_timeout(conn.incr(&key, delta)).await?;
247 Ok(new_value)
248 }
249
250 async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
251 let key = self.build_key(key);
252 let mut conn = self.connection.clone();
253 let new_value: i64 = self.with_op_timeout(conn.decr(&key, delta)).await?;
254 Ok(new_value)
255 }
256
257 async fn mget(&self, keys: &[&str]) -> CacheResult<Vec<Option<String>>> {
262 if keys.is_empty() {
263 return Ok(Vec::new());
264 }
265 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
266 trace!("Cache MGET: {} keys", full_keys.len());
267 let mut conn = self.connection.clone();
268 let values: Vec<Option<String>> = self
269 .with_op_timeout(redis::cmd("MGET").arg(&full_keys).query_async(&mut conn))
270 .await?;
271 Ok(values)
272 }
273
274 async fn mset(&self, items: &[(&str, String)], ttl: Option<Duration>) -> CacheResult<()> {
281 if items.is_empty() {
282 return Ok(());
283 }
284 let mut conn = self.connection.clone();
285 let ttl = ttl.or(self.config.default_ttl);
286
287 if let Some(ttl) = ttl {
288 let ttl_seconds = ttl.as_secs();
289 trace!("Cache MSET (pipelined SET EX): {} items", items.len());
290 let mut pipe = redis::pipe();
291 for (key, value) in items {
292 pipe.cmd("SET")
293 .arg(self.build_key(key))
294 .arg(value)
295 .arg("EX")
296 .arg(ttl_seconds)
297 .ignore();
298 }
299 let _: () = self.with_op_timeout(pipe.query_async(&mut conn)).await?;
300 } else {
301 trace!("Cache MSET: {} items", items.len());
302 let mut cmd = redis::cmd("MSET");
303 for (key, value) in items {
304 cmd.arg(self.build_key(key)).arg(value);
305 }
306 let _: () = self.with_op_timeout(cmd.query_async(&mut conn)).await?;
307 }
308 Ok(())
309 }
310
311 async fn mdel(&self, keys: &[&str]) -> CacheResult<()> {
313 if keys.is_empty() {
314 return Ok(());
315 }
316 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
317 trace!("Cache DEL: {} keys", full_keys.len());
318 let mut conn = self.connection.clone();
319 let _: () = self
320 .with_op_timeout(redis::cmd("DEL").arg(&full_keys).query_async(&mut conn))
321 .await?;
322 Ok(())
323 }
324
325 fn supports_atomic_sets(&self) -> bool {
328 true
329 }
330
331 async fn set_add(&self, set_key: &str, member: &str) -> CacheResult<()> {
336 let set_key = self.build_key(set_key);
337 let mut conn = self.connection.clone();
338 let _: () = self.with_op_timeout(conn.sadd(&set_key, member)).await?;
339 Ok(())
340 }
341
342 async fn set_remove(&self, set_key: &str, member: &str) -> CacheResult<()> {
344 let set_key = self.build_key(set_key);
345 let mut conn = self.connection.clone();
346 let _: () = self.with_op_timeout(conn.srem(&set_key, member)).await?;
347 Ok(())
348 }
349
350 async fn set_add_many(&self, set_key: &str, members: &[&str]) -> CacheResult<()> {
353 if members.is_empty() {
354 return Ok(());
355 }
356 let set_key = self.build_key(set_key);
357 let mut conn = self.connection.clone();
358 let _: () = self.with_op_timeout(conn.sadd(&set_key, members)).await?;
359 Ok(())
360 }
361
362 async fn set_remove_many(&self, set_key: &str, members: &[&str]) -> CacheResult<()> {
364 if members.is_empty() {
365 return Ok(());
366 }
367 let set_key = self.build_key(set_key);
368 let mut conn = self.connection.clone();
369 let _: () = self.with_op_timeout(conn.srem(&set_key, members)).await?;
370 Ok(())
371 }
372
373 async fn set_members(&self, set_key: &str) -> CacheResult<Vec<String>> {
375 let set_key = self.build_key(set_key);
376 let mut conn = self.connection.clone();
377 let members: Vec<String> = self.with_op_timeout(conn.smembers(&set_key)).await?;
378 Ok(members)
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_build_key() {
388 let config = CacheConfig::redis("redis://localhost:6379")
389 .unwrap()
390 .with_key_prefix("test");
391
392 assert_eq!(config.build_key("key"), "test:key");
395 }
396}