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!(
40 "Connecting to Redis cache: {}",
41 crate::config::redact_url_for_logging(&config.url)
42 );
43 let client =
44 Client::open(config.url.as_str()).map_err(|e| CacheError::Connection(e.to_string()))?;
45
46 let mut manager_config =
59 ConnectionManagerConfig::new().set_connection_timeout(Some(config.connection_timeout));
60 if config.max_connections > 0 {
61 manager_config = manager_config.set_concurrency_limit(config.max_connections);
62 }
63
64 let connection = ConnectionManager::new_with_config(client, manager_config)
65 .await
66 .map_err(|e| CacheError::Connection(e.to_string()))?;
67
68 debug!("Redis cache connection established");
69 Ok(Self { connection, config })
70 }
71
72 pub fn connection(&self) -> &ConnectionManager {
74 &self.connection
75 }
76
77 fn build_key(&self, key: &str) -> String {
79 self.config.build_key(key)
80 }
81
82 async fn with_op_timeout<F, T>(&self, fut: F) -> CacheResult<T>
89 where
90 F: Future<Output = redis::RedisResult<T>>,
91 {
92 match tokio::time::timeout(self.config.operation_timeout, fut).await {
93 Ok(result) => Ok(result?),
94 Err(_) => Err(CacheError::Timeout),
95 }
96 }
97
98 async fn scan_and_unlink(
106 conn: &mut ConnectionManager,
107 pattern: String,
108 ) -> redis::RedisResult<()> {
109 use futures::StreamExt;
110
111 const UNLINK_BATCH_SIZE: usize = 500;
113
114 let mut matched: Vec<String> = Vec::new();
115 {
116 let mut iter: redis::AsyncIter<'_, String> = conn.scan_match(pattern.as_str()).await?;
117 while let Some(key) = iter.next().await {
118 matched.push(key?);
119 }
120 }
121
122 for chunk in matched.chunks(UNLINK_BATCH_SIZE) {
123 let _: () = redis::cmd("UNLINK").arg(chunk).query_async(conn).await?;
124 }
125
126 Ok(())
127 }
128}
129
130#[async_trait]
131impl CacheStore for RedisCache {
132 async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
133 let key = self.build_key(key);
134 trace!("Cache GET: {}", key);
135 let mut conn = self.connection.clone();
136
137 let value: Option<String> = self.with_op_timeout(conn.get(&key)).await?;
138 trace!(
139 "Cache {} for: {}",
140 if value.is_some() { "HIT" } else { "MISS" },
141 key
142 );
143 Ok(value)
144 }
145
146 async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
147 let key = self.build_key(key);
148 trace!("Cache SET: {} (ttl: {:?})", key, ttl);
149 let mut conn = self.connection.clone();
150
151 let ttl = ttl.or(self.config.default_ttl);
152
153 if let Some(ttl) = ttl {
154 let ttl_seconds = ttl.as_secs();
155 let _: () = self
156 .with_op_timeout(conn.set_ex(&key, value, ttl_seconds))
157 .await?;
158 } else {
159 let _: () = self.with_op_timeout(conn.set(&key, value)).await?;
160 }
161
162 Ok(())
163 }
164
165 async fn set_json_forever(&self, key: &str, value: String) -> CacheResult<()> {
168 let key = self.build_key(key);
169 trace!("Cache SET (no expiry): {}", key);
170 let mut conn = self.connection.clone();
171 let _: () = self.with_op_timeout(conn.set(&key, value)).await?;
172 Ok(())
173 }
174
175 async fn delete(&self, key: &str) -> CacheResult<()> {
176 let key = self.build_key(key);
177 let mut conn = self.connection.clone();
178 let _: () = self.with_op_timeout(conn.del(&key)).await?;
179 Ok(())
180 }
181
182 async fn exists(&self, key: &str) -> CacheResult<bool> {
183 let key = self.build_key(key);
184 let mut conn = self.connection.clone();
185 let exists: bool = self.with_op_timeout(conn.exists(&key)).await?;
186 Ok(exists)
187 }
188
189 async fn clear(&self) -> CacheResult<()> {
205 match self.config.key_prefix.as_deref() {
206 Some(prefix) if !prefix.is_empty() => {
207 let pattern = format!("{prefix}:*");
208 let mut conn = self.connection.clone();
209 self.with_op_timeout(Self::scan_and_unlink(&mut conn, pattern))
210 .await?;
211 }
212 _ => {
213 let mut conn = self.connection.clone();
214 let _: () = self
215 .with_op_timeout(redis::cmd("FLUSHDB").query_async(&mut conn))
216 .await?;
217 }
218 }
219 Ok(())
220 }
221
222 async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
223 let key = self.build_key(key);
224 let mut conn = self.connection.clone();
225
226 let ttl_seconds: i64 = self.with_op_timeout(conn.ttl(&key)).await?;
227
228 match ttl_seconds {
229 -2 => Ok(None), -1 => Ok(None), seconds if seconds > 0 => Ok(Some(Duration::from_secs(seconds as u64))),
232 _ => Ok(None),
233 }
234 }
235
236 async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
237 let key = self.build_key(key);
238 let mut conn = self.connection.clone();
239 let ttl_seconds = ttl.as_secs();
240 let _: () = self
241 .with_op_timeout(conn.expire(&key, ttl_seconds as i64))
242 .await?;
243 Ok(())
244 }
245
246 async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
247 let key = self.build_key(key);
248 let mut conn = self.connection.clone();
249 let new_value: i64 = self.with_op_timeout(conn.incr(&key, delta)).await?;
250 Ok(new_value)
251 }
252
253 async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
254 let key = self.build_key(key);
255 let mut conn = self.connection.clone();
256 let new_value: i64 = self.with_op_timeout(conn.decr(&key, delta)).await?;
257 Ok(new_value)
258 }
259
260 async fn mget(&self, keys: &[&str]) -> CacheResult<Vec<Option<String>>> {
265 if keys.is_empty() {
266 return Ok(Vec::new());
267 }
268 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
269 trace!("Cache MGET: {} keys", full_keys.len());
270 let mut conn = self.connection.clone();
271 let values: Vec<Option<String>> = self
272 .with_op_timeout(redis::cmd("MGET").arg(&full_keys).query_async(&mut conn))
273 .await?;
274 Ok(values)
275 }
276
277 async fn mset(&self, items: &[(&str, String)], ttl: Option<Duration>) -> CacheResult<()> {
284 if items.is_empty() {
285 return Ok(());
286 }
287 let mut conn = self.connection.clone();
288 let ttl = ttl.or(self.config.default_ttl);
289
290 if let Some(ttl) = ttl {
291 let ttl_seconds = ttl.as_secs();
292 trace!("Cache MSET (pipelined SET EX): {} items", items.len());
293 let mut pipe = redis::pipe();
294 for (key, value) in items {
295 pipe.cmd("SET")
296 .arg(self.build_key(key))
297 .arg(value)
298 .arg("EX")
299 .arg(ttl_seconds)
300 .ignore();
301 }
302 let _: () = self.with_op_timeout(pipe.query_async(&mut conn)).await?;
303 } else {
304 trace!("Cache MSET: {} items", items.len());
305 let mut cmd = redis::cmd("MSET");
306 for (key, value) in items {
307 cmd.arg(self.build_key(key)).arg(value);
308 }
309 let _: () = self.with_op_timeout(cmd.query_async(&mut conn)).await?;
310 }
311 Ok(())
312 }
313
314 async fn mdel(&self, keys: &[&str]) -> CacheResult<()> {
316 if keys.is_empty() {
317 return Ok(());
318 }
319 let full_keys: Vec<String> = keys.iter().map(|k| self.build_key(k)).collect();
320 trace!("Cache DEL: {} keys", full_keys.len());
321 let mut conn = self.connection.clone();
322 let _: () = self
323 .with_op_timeout(redis::cmd("DEL").arg(&full_keys).query_async(&mut conn))
324 .await?;
325 Ok(())
326 }
327
328 fn supports_atomic_sets(&self) -> bool {
331 true
332 }
333
334 async fn set_add(&self, set_key: &str, member: &str) -> CacheResult<()> {
339 let set_key = self.build_key(set_key);
340 let mut conn = self.connection.clone();
341 let _: () = self.with_op_timeout(conn.sadd(&set_key, member)).await?;
342 Ok(())
343 }
344
345 async fn set_remove(&self, set_key: &str, member: &str) -> CacheResult<()> {
347 let set_key = self.build_key(set_key);
348 let mut conn = self.connection.clone();
349 let _: () = self.with_op_timeout(conn.srem(&set_key, member)).await?;
350 Ok(())
351 }
352
353 async fn set_add_many(&self, set_key: &str, members: &[&str]) -> CacheResult<()> {
356 if members.is_empty() {
357 return Ok(());
358 }
359 let set_key = self.build_key(set_key);
360 let mut conn = self.connection.clone();
361 let _: () = self.with_op_timeout(conn.sadd(&set_key, members)).await?;
362 Ok(())
363 }
364
365 async fn set_remove_many(&self, set_key: &str, members: &[&str]) -> CacheResult<()> {
367 if members.is_empty() {
368 return Ok(());
369 }
370 let set_key = self.build_key(set_key);
371 let mut conn = self.connection.clone();
372 let _: () = self.with_op_timeout(conn.srem(&set_key, members)).await?;
373 Ok(())
374 }
375
376 async fn set_members(&self, set_key: &str) -> CacheResult<Vec<String>> {
378 let set_key = self.build_key(set_key);
379 let mut conn = self.connection.clone();
380 let members: Vec<String> = self.with_op_timeout(conn.smembers(&set_key)).await?;
381 Ok(members)
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_build_key() {
391 let config = CacheConfig::redis("redis://localhost:6379")
392 .unwrap()
393 .with_key_prefix("test");
394
395 assert_eq!(config.build_key("key"), "test:key");
398 }
399}