Skip to main content

armature_cache/
traits.rs

1//! Cache store trait definition.
2
3use crate::error::{CacheError, CacheResult};
4use async_trait::async_trait;
5use std::time::Duration;
6
7/// Cache store trait for different cache backends.
8#[async_trait]
9pub trait CacheStore: Send + Sync {
10    /// Get a JSON value from the cache.
11    ///
12    /// # Arguments
13    ///
14    /// * `key` - The cache key
15    ///
16    /// # Returns
17    ///
18    /// Returns `Ok(Some(value))` if the key exists, `Ok(None)` if not found,
19    /// or an error if the operation fails.
20    async fn get_json(&self, key: &str) -> CacheResult<Option<String>>;
21
22    /// Set a JSON value in the cache.
23    ///
24    /// # Arguments
25    ///
26    /// * `key` - The cache key
27    /// * `value` - The JSON string value
28    /// * `ttl` - Optional time-to-live duration
29    async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()>;
30
31    /// Delete a key from the cache.
32    ///
33    /// # Arguments
34    ///
35    /// * `key` - The cache key to delete
36    async fn delete(&self, key: &str) -> CacheResult<()>;
37
38    /// Check if a key exists in the cache.
39    ///
40    /// # Arguments
41    ///
42    /// * `key` - The cache key to check
43    async fn exists(&self, key: &str) -> CacheResult<bool>;
44
45    /// Clear all keys from the cache.
46    ///
47    /// **Warning:** This operation may be destructive and affect all keys.
48    async fn clear(&self) -> CacheResult<()>;
49
50    /// Get the TTL (time-to-live) of a key.
51    ///
52    /// # Arguments
53    ///
54    /// * `key` - The cache key
55    ///
56    /// # Returns
57    ///
58    /// Returns `Ok(Some(duration))` if the key has a TTL, `Ok(None)` if the key
59    /// has no expiration or doesn't exist.
60    async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>>;
61
62    /// Set or update the expiration time for a key.
63    ///
64    /// # Arguments
65    ///
66    /// * `key` - The cache key
67    /// * `ttl` - The new time-to-live duration
68    async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()>;
69
70    /// Increment a numeric value.
71    ///
72    /// # Arguments
73    ///
74    /// * `key` - The cache key
75    /// * `delta` - The amount to increment by
76    ///
77    /// # Returns
78    ///
79    /// Returns the new value after incrementing.
80    async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64>;
81
82    /// Decrement a numeric value.
83    ///
84    /// # Arguments
85    ///
86    /// * `key` - The cache key
87    /// * `delta` - The amount to decrement by
88    ///
89    /// # Returns
90    ///
91    /// Returns the new value after decrementing.
92    async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64>;
93
94    // ========== Native Batch Primitives ==========
95    //
96    // These are the low-level batch operations that backends can override with
97    // a single native command (e.g. Redis `MGET`/`MSET`/variadic `DEL`) to turn
98    // N round-trips into one. The DEFAULT implementations fall back to the
99    // per-key loop (run concurrently), so existing `CacheStore` impls keep
100    // working unchanged. The higher-level `get_many`/`set_many`/`delete_many`
101    // methods and `ParallelCacheOps` delegate here, so overriding these three
102    // methods is enough to accelerate all batch APIs.
103
104    /// Get multiple keys in a single batch operation.
105    ///
106    /// Returns a vector of `Option<String>` in the **same order** as `keys`;
107    /// `None` indicates a missing key.
108    ///
109    /// The default implementation issues one `get_json` per key concurrently;
110    /// backends should override this with a native multi-get (e.g. `MGET`).
111    async fn mget(&self, keys: &[&str]) -> CacheResult<Vec<Option<String>>> {
112        use futures::future::try_join_all;
113
114        let futures = keys.iter().map(|key| self.get_json(key));
115        try_join_all(futures).await
116    }
117
118    /// Set multiple key/value pairs in a single batch operation.
119    ///
120    /// The default implementation issues one `set_json` per pair concurrently;
121    /// backends should override this with a native multi-set (e.g. `MSET`, or a
122    /// pipeline of `SET ... EX` when a TTL is required).
123    async fn mset(&self, items: &[(&str, String)], ttl: Option<Duration>) -> CacheResult<()> {
124        use futures::future::try_join_all;
125
126        let futures = items
127            .iter()
128            .map(|(key, value)| self.set_json(key, value.clone(), ttl));
129        try_join_all(futures).await?;
130        Ok(())
131    }
132
133    /// Delete multiple keys in a single batch operation.
134    ///
135    /// The default implementation issues one `delete` per key concurrently;
136    /// backends should override this with a variadic `DEL`/`UNLINK`.
137    async fn mdel(&self, keys: &[&str]) -> CacheResult<()> {
138        use futures::future::try_join_all;
139
140        let futures = keys.iter().map(|key| self.delete(key));
141        try_join_all(futures).await?;
142        Ok(())
143    }
144
145    // ========== Batch Operations (Parallel) ==========
146
147    /// Get multiple keys in parallel.
148    ///
149    /// This operation fetches multiple cache keys concurrently, significantly
150    /// reducing total latency compared to sequential gets.
151    ///
152    /// # Arguments
153    ///
154    /// * `keys` - Slice of cache keys to fetch
155    ///
156    /// # Returns
157    ///
158    /// Returns a vector of `Option<String>` in the same order as the input keys.
159    /// `None` indicates the key was not found.
160    ///
161    /// # Performance
162    ///
163    /// - **Sequential:** O(n * network_latency)
164    /// - **Parallel:** O(max(network_latencies)) ≈ O(network_latency)
165    /// - **Speedup:** 10-100x for network-bound operations
166    ///
167    /// # Examples
168    ///
169    /// ```no_run
170    /// # use armature_cache::*;
171    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
172    /// // Fetch 100 user profiles in parallel
173    /// let keys: Vec<String> = (1..=100).map(|i| format!("user:{}", i)).collect();
174    /// let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();
175    /// let profiles = cache.get_many(&key_refs).await?;
176    ///
177    /// // Sequential: ~1000ms (10ms * 100)
178    /// // Parallel:   ~15ms (max of all parallel requests)
179    /// # Ok(())
180    /// # }
181    /// ```
182    async fn get_many(&self, keys: &[&str]) -> CacheResult<Vec<Option<String>>> {
183        // Delegate to the native batch primitive so backends that override
184        // `mget` (e.g. Redis `MGET`) accelerate this path automatically.
185        self.mget(keys).await
186    }
187
188    /// Set multiple key-value pairs in parallel.
189    ///
190    /// # Arguments
191    ///
192    /// * `items` - Slice of (key, value) tuples
193    /// * `ttl` - Optional time-to-live for all keys
194    ///
195    /// # Performance
196    ///
197    /// 10-100x faster than sequential sets for network-bound operations.
198    ///
199    /// # Examples
200    ///
201    /// ```no_run
202    /// # use armature_cache::*;
203    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
204    /// use std::time::Duration;
205    ///
206    /// let items = vec![
207    ///     ("user:1", r#"{"name":"Alice"}"#.to_string()),
208    ///     ("user:2", r#"{"name":"Bob"}"#.to_string()),
209    /// ];
210    ///
211    /// cache.set_many(&items, Some(Duration::from_secs(3600))).await?;
212    /// # Ok(())
213    /// # }
214    /// ```
215    async fn set_many(&self, items: &[(&str, String)], ttl: Option<Duration>) -> CacheResult<()> {
216        // Delegate to the native batch primitive (e.g. Redis `MSET`/pipeline).
217        self.mset(items, ttl).await
218    }
219
220    /// Delete multiple keys in parallel.
221    ///
222    /// # Arguments
223    ///
224    /// * `keys` - Slice of cache keys to delete
225    ///
226    /// # Performance
227    ///
228    /// 10-100x faster than sequential deletes.
229    ///
230    /// # Examples
231    ///
232    /// ```no_run
233    /// # use armature_cache::*;
234    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
235    /// // Bulk cache invalidation
236    /// let keys = vec!["session:1", "session:2", "session:3"];
237    /// cache.delete_many(&keys).await?;
238    /// # Ok(())
239    /// # }
240    /// ```
241    async fn delete_many(&self, keys: &[&str]) -> CacheResult<()> {
242        // Delegate to the native batch primitive (e.g. Redis variadic `DEL`).
243        self.mdel(keys).await
244    }
245
246    /// Check existence of multiple keys in parallel.
247    ///
248    /// # Arguments
249    ///
250    /// * `keys` - Slice of cache keys to check
251    ///
252    /// # Returns
253    ///
254    /// Returns a vector of booleans in the same order as input keys.
255    ///
256    /// # Examples
257    ///
258    /// ```no_run
259    /// # use armature_cache::*;
260    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
261    /// let keys = vec!["user:1", "user:2", "user:3"];
262    /// let exists = cache.exists_many(&keys).await?;
263    ///
264    /// for (key, exists) in keys.iter().zip(exists.iter()) {
265    ///     println!("{}: {}", key, exists);
266    /// }
267    /// # Ok(())
268    /// # }
269    /// ```
270    async fn exists_many(&self, keys: &[&str]) -> CacheResult<Vec<bool>> {
271        use futures::future::try_join_all;
272
273        let futures = keys.iter().map(|key| self.exists(key));
274        try_join_all(futures).await
275    }
276
277    /// Get TTL for multiple keys in parallel.
278    ///
279    /// # Arguments
280    ///
281    /// * `keys` - Slice of cache keys
282    ///
283    /// # Returns
284    ///
285    /// Returns a vector of `Option<Duration>` for each key.
286    ///
287    /// # Examples
288    ///
289    /// ```no_run
290    /// # use armature_cache::*;
291    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
292    /// let keys = vec!["session:1", "session:2"];
293    /// let ttls = cache.ttl_many(&keys).await?;
294    ///
295    /// for (key, ttl) in keys.iter().zip(ttls.iter()) {
296    ///     match ttl {
297    ///         Some(duration) => println!("{}: expires in {:?}", key, duration),
298    ///         None => println!("{}: no expiration", key),
299    ///     }
300    /// }
301    /// # Ok(())
302    /// # }
303    /// ```
304    async fn ttl_many(&self, keys: &[&str]) -> CacheResult<Vec<Option<Duration>>> {
305        use futures::future::try_join_all;
306
307        let futures = keys.iter().map(|key| self.ttl(key));
308        try_join_all(futures).await
309    }
310
311    // ========== Set Primitives (persistent, cross-instance indexes) ==========
312    //
313    // Low-level primitives for maintaining a set-of-strings value at a given
314    // key *in the backing store itself*. These exist so higher-level indexes
315    // built on top of a `CacheStore` — e.g. `TaggedCache`'s tag -> member-key
316    // index (see `crate::invalidation`) — are visible to every process /
317    // instance sharing that store, not just the process that wrote them.
318    //
319    // The DEFAULT implementations below are a portable but NON-ATOMIC
320    // read-modify-write layered on `get_json`/`set_json`: correct for a single
321    // writer or low-contention use, but concurrent `set_add`/`set_remove`
322    // calls against the SAME `set_key` from different instances can race and
323    // lose an update (last write wins). Backends with a native set type
324    // should override these three methods for atomicity — `RedisCache` does,
325    // via `SADD`/`SREM`/`SMEMBERS`.
326
327    /// Whether this backend's [`Self::set_add`]/[`Self::set_remove`]/
328    /// [`Self::set_members`] are backed by a native, atomic set type rather
329    /// than the trait's default non-atomic read-modify-write.
330    ///
331    /// `RedisCache` overrides this to return `true` (its implementations use
332    /// `SADD`/`SREM`/`SMEMBERS`). Every other backend — including
333    /// `InMemoryCache` and `MemcachedCache` — keeps this default `false`,
334    /// since they inherit the default set primitives above.
335    ///
336    /// [`crate::invalidation::TaggedCache::new`] checks this capability and
337    /// logs a warning once, at construction time, when the backing store
338    /// answers `false` — giving operators a runtime signal (not just a doc
339    /// comment) that concurrent tag-index updates against that deployment
340    /// can race and silently lose an update.
341    fn supports_atomic_sets(&self) -> bool {
342        false
343    }
344
345    /// Add `member` to the persistent string set stored at `set_key`.
346    ///
347    /// A no-op if `member` is already present.
348    ///
349    /// # Examples
350    ///
351    /// ```no_run
352    /// # use armature_cache::*;
353    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
354    /// // Build a persisted set of member keys under "tag:users".
355    /// cache.set_add("tag:users", "user:1").await?;
356    /// cache.set_add("tag:users", "user:2").await?;
357    /// cache.set_add("tag:users", "user:1").await?; // duplicate: no-op
358    ///
359    /// let members = cache.set_members("tag:users").await?;
360    /// assert_eq!(members.len(), 2);
361    /// # Ok(())
362    /// # }
363    /// ```
364    async fn set_add(&self, set_key: &str, member: &str) -> CacheResult<()> {
365        let mut members = self.set_members(set_key).await?;
366        if !members.iter().any(|m| m == member) {
367            members.push(member.to_string());
368            let json = serde_json::to_string(&members)
369                .map_err(|e| CacheError::Serialization(e.to_string()))?;
370            self.set_json(set_key, json, None).await?;
371        }
372        Ok(())
373    }
374
375    /// Remove `member` from the persistent string set stored at `set_key`.
376    ///
377    /// A no-op if `set_key` or `member` doesn't exist. Deletes `set_key`
378    /// entirely once its last member is removed, so an emptied set doesn't
379    /// linger as a zero-length entry.
380    ///
381    /// # Examples
382    ///
383    /// ```no_run
384    /// # use armature_cache::*;
385    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
386    /// cache.set_add("tag:users", "user:1").await?;
387    /// cache.set_remove("tag:users", "user:1").await?;
388    ///
389    /// // The set is now empty; `set_key` itself is removed rather than left
390    /// // behind as a zero-length entry.
391    /// assert!(cache.set_members("tag:users").await?.is_empty());
392    /// # Ok(())
393    /// # }
394    /// ```
395    async fn set_remove(&self, set_key: &str, member: &str) -> CacheResult<()> {
396        let mut members = self.set_members(set_key).await?;
397        let before = members.len();
398        members.retain(|m| m != member);
399
400        if members.len() != before {
401            if members.is_empty() {
402                self.delete(set_key).await?;
403            } else {
404                let json = serde_json::to_string(&members)
405                    .map_err(|e| CacheError::Serialization(e.to_string()))?;
406                self.set_json(set_key, json, None).await?;
407            }
408        }
409        Ok(())
410    }
411
412    /// Read every member of the persistent string set stored at `set_key`.
413    ///
414    /// Returns an empty `Vec` if `set_key` doesn't exist.
415    ///
416    /// # Examples
417    ///
418    /// ```no_run
419    /// # use armature_cache::*;
420    /// # async fn example(cache: &impl CacheStore) -> CacheResult<()> {
421    /// cache.set_add("tag:users", "user:1").await?;
422    /// cache.set_add("tag:users", "user:2").await?;
423    ///
424    /// let mut members = cache.set_members("tag:users").await?;
425    /// members.sort();
426    /// assert_eq!(members, vec!["user:1".to_string(), "user:2".to_string()]);
427    ///
428    /// // A set_key that was never written returns an empty Vec.
429    /// assert!(cache.set_members("tag:unused").await?.is_empty());
430    /// # Ok(())
431    /// # }
432    /// ```
433    async fn set_members(&self, set_key: &str) -> CacheResult<Vec<String>> {
434        match self.get_json(set_key).await? {
435            Some(json) => {
436                serde_json::from_str(&json).map_err(|e| CacheError::Deserialization(e.to_string()))
437            }
438            None => Ok(Vec::new()),
439        }
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use crate::tiered::InMemoryCache;
447
448    // `InMemoryCache` does NOT override `mget`/`mset`/`mdel`, so these tests
449    // exercise the trait's DEFAULT (per-key loop) implementations.
450
451    #[tokio::test]
452    async fn test_mget_default_fallback_preserves_order() {
453        let cache = InMemoryCache::new();
454        cache.set_json("a", "1".to_string(), None).await.unwrap();
455        cache.set_json("c", "3".to_string(), None).await.unwrap();
456
457        let got = cache.mget(&["a", "b", "c"]).await.unwrap();
458        assert_eq!(
459            got,
460            vec![Some("1".to_string()), None, Some("3".to_string())]
461        );
462    }
463
464    #[tokio::test]
465    async fn test_mset_and_mdel_default_fallback() {
466        let cache = InMemoryCache::new();
467        cache
468            .mset(&[("x", "10".to_string()), ("y", "20".to_string())], None)
469            .await
470            .unwrap();
471        assert_eq!(cache.get_json("x").await.unwrap(), Some("10".to_string()));
472        assert_eq!(cache.get_json("y").await.unwrap(), Some("20".to_string()));
473
474        cache.mdel(&["x", "y"]).await.unwrap();
475        assert_eq!(cache.get_json("x").await.unwrap(), None);
476        assert_eq!(cache.get_json("y").await.unwrap(), None);
477    }
478
479    #[tokio::test]
480    async fn test_public_batch_methods_delegate_to_primitives() {
481        let cache = InMemoryCache::new();
482        cache.set_json("k1", "v1".to_string(), None).await.unwrap();
483
484        // get_many delegates to mget
485        let got = cache.get_many(&["k1", "k2"]).await.unwrap();
486        assert_eq!(got, vec![Some("v1".to_string()), None]);
487
488        // set_many delegates to mset
489        cache
490            .set_many(&[("k3", "v3".to_string())], None)
491            .await
492            .unwrap();
493        assert_eq!(cache.get_json("k3").await.unwrap(), Some("v3".to_string()));
494
495        // delete_many delegates to mdel
496        cache.delete_many(&["k1", "k3"]).await.unwrap();
497        assert_eq!(cache.get_json("k1").await.unwrap(), None);
498        assert_eq!(cache.get_json("k3").await.unwrap(), None);
499    }
500
501    #[tokio::test]
502    async fn test_mget_empty_keys() {
503        let cache = InMemoryCache::new();
504        let got = cache.mget(&[]).await.unwrap();
505        assert!(got.is_empty());
506    }
507
508    /// Regression for Finding 3: backends that don't override the default,
509    /// non-atomic `set_add`/`set_remove`/`set_members` must report
510    /// `supports_atomic_sets() == false` so callers (e.g. `TaggedCache::new`)
511    /// can warn operators. `InMemoryCache` never overrides these, so it must
512    /// keep the trait's default answer.
513    #[tokio::test]
514    async fn test_supports_atomic_sets_default_is_false() {
515        let cache = InMemoryCache::new();
516        assert!(!cache.supports_atomic_sets());
517    }
518}