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