Skip to main content

dbkit_rs/
cache.rs

1use dashmap::DashMap;
2use std::hash::Hash;
3use std::sync::Arc;
4
5/// Concurrent key-value cache with named buckets.
6///
7/// Uses DashMap internally for lock-free concurrent reads. Create named
8/// buckets for different entity types (products, listings, etc.).
9///
10/// Both key and value types default to `String`, so `Cache` is a drop-in
11/// replacement for the previous untyped cache. Use `Cache<i32, MyStruct>`,
12/// `Cache<String, Vec<u8>>`, etc. for typed buckets.
13#[derive(Debug)]
14pub struct Cache<K = String, V = String>
15where
16    K: Clone + Eq + Hash + Send + Sync + 'static,
17    V: Clone + Send + Sync + 'static,
18{
19    buckets: Arc<DashMap<String, Arc<DashMap<K, V>>>>,
20}
21
22impl<K, V> Clone for Cache<K, V>
23where
24    K: Clone + Eq + Hash + Send + Sync + 'static,
25    V: Clone + Send + Sync + 'static,
26{
27    fn clone(&self) -> Self {
28        Self {
29            buckets: Arc::clone(&self.buckets),
30        }
31    }
32}
33
34impl<K, V> Cache<K, V>
35where
36    K: Clone + Eq + Hash + Send + Sync + 'static,
37    V: Clone + Send + Sync + 'static,
38{
39    pub fn new() -> Self {
40        Self {
41            buckets: Arc::new(DashMap::new()),
42        }
43    }
44
45    /// Create a set of named buckets upfront.
46    pub fn with_buckets(names: &[&str]) -> Self {
47        let cache = Self::new();
48        for name in names {
49            cache
50                .buckets
51                .insert(name.to_string(), Arc::new(DashMap::new()));
52        }
53        cache
54    }
55
56    /// Get a value from a named bucket.
57    pub fn get(&self, bucket: &str, key: &K) -> Option<V> {
58        self.buckets
59            .get(bucket)
60            .and_then(|b| b.get(key).map(|v| v.clone()))
61    }
62
63    /// Set a value in a named bucket (creates the bucket if it doesn't exist).
64    pub fn set(&self, bucket: &str, key: K, value: V) {
65        self.buckets
66            .entry(bucket.to_string())
67            .or_insert_with(|| Arc::new(DashMap::new()))
68            .insert(key, value);
69    }
70
71    /// Remove a value from a named bucket.
72    pub fn remove(&self, bucket: &str, key: &K) -> Option<V> {
73        self.buckets
74            .get(bucket)
75            .and_then(|b| b.remove(key).map(|(_, v)| v))
76    }
77
78    /// Clear all entries in a specific bucket.
79    pub fn clear_bucket(&self, bucket: &str) {
80        if let Some(b) = self.buckets.get(bucket) {
81            b.clear();
82        }
83    }
84
85    /// Clear all buckets.
86    pub fn clear_all(&self) {
87        for entry in self.buckets.iter() {
88            entry.value().clear();
89        }
90    }
91}
92
93impl<K, V> Default for Cache<K, V>
94where
95    K: Clone + Eq + Hash + Send + Sync + 'static,
96    V: Clone + Send + Sync + 'static,
97{
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_cache_basic() {
109        let cache: Cache = Cache::with_buckets(&["products", "listings"]);
110
111        cache.set("products", "upc:123".into(), "uuid-1".into());
112        assert_eq!(cache.get("products", &"upc:123".into()), Some("uuid-1".into()));
113        assert_eq!(cache.get("products", &"upc:999".into()), None);
114        assert_eq!(cache.get("listings", &"upc:123".into()), None);
115    }
116
117    #[test]
118    fn test_cache_auto_create_bucket() {
119        let cache: Cache = Cache::new();
120        cache.set("new_bucket", "key".into(), "value".into());
121        assert_eq!(cache.get("new_bucket", &"key".into()), Some("value".into()));
122    }
123
124    #[test]
125    fn test_cache_clear() {
126        let cache: Cache = Cache::with_buckets(&["a", "b"]);
127        cache.set("a", "k1".into(), "v1".into());
128        cache.set("b", "k2".into(), "v2".into());
129
130        cache.clear_bucket("a");
131        assert_eq!(cache.get("a", &"k1".into()), None);
132        assert_eq!(cache.get("b", &"k2".into()), Some("v2".into()));
133
134        cache.clear_all();
135        assert_eq!(cache.get("b", &"k2".into()), None);
136    }
137
138    #[test]
139    fn test_typed_cache_i32_values() {
140        let cache: Cache<String, i32> = Cache::with_buckets(&["counts"]);
141
142        cache.set("counts", "page_views".into(), 42);
143        cache.set("counts", "sessions".into(), 7);
144
145        assert_eq!(cache.get("counts", &"page_views".into()), Some(42));
146        assert_eq!(cache.get("counts", &"sessions".into()), Some(7));
147        assert_eq!(cache.get("counts", &"missing".into()), None);
148
149        cache.remove("counts", &"page_views".into());
150        assert_eq!(cache.get("counts", &"page_views".into()), None);
151    }
152
153    #[test]
154    fn test_typed_cache_i32_keys() {
155        let cache: Cache<i32, String> = Cache::with_buckets(&["users"]);
156
157        cache.set("users", 1, "alice".into());
158        cache.set("users", 2, "bob".into());
159
160        assert_eq!(cache.get("users", &1), Some("alice".into()));
161        assert_eq!(cache.get("users", &2), Some("bob".into()));
162        assert_eq!(cache.get("users", &3), None);
163    }
164
165    #[test]
166    fn test_typed_cache_both_generic() {
167        let cache: Cache<i32, f64> = Cache::new();
168
169        cache.set("scores", 100, 9.5);
170        cache.set("scores", 200, 8.3);
171
172        assert_eq!(cache.get("scores", &100), Some(9.5));
173        assert_eq!(cache.get("scores", &200), Some(8.3));
174    }
175
176    #[test]
177    fn test_typed_cache_vec() {
178        let cache: Cache<String, Vec<String>> = Cache::new();
179
180        cache.set(
181            "tags",
182            "item:1".into(),
183            vec!["rust".into(), "database".into()],
184        );
185
186        let tags = cache.get("tags", &"item:1".into()).unwrap();
187        assert_eq!(tags, vec!["rust".to_string(), "database".to_string()]);
188    }
189
190    #[test]
191    fn test_typed_cache_struct() {
192        #[derive(Debug, Clone, PartialEq)]
193        struct Product {
194            id: i32,
195            name: String,
196        }
197
198        let cache: Cache<String, Product> = Cache::new();
199        cache.set(
200            "products",
201            "sku:abc".into(),
202            Product {
203                id: 1,
204                name: "Widget".into(),
205            },
206        );
207
208        let product = cache.get("products", &"sku:abc".into()).unwrap();
209        assert_eq!(product.id, 1);
210        assert_eq!(product.name, "Widget");
211    }
212}