Skip to main content

dbkit/
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        // Fast path: the bucket already exists (the steady state), so look it
66        // up by &str without allocating the owned String the entry API needs.
67        if let Some(b) = self.buckets.get(bucket) {
68            b.insert(key, value);
69            return;
70        }
71        self.buckets
72            .entry(bucket.to_string())
73            .or_insert_with(|| Arc::new(DashMap::new()))
74            .insert(key, value);
75    }
76
77    /// Remove a value from a named bucket.
78    pub fn remove(&self, bucket: &str, key: &K) -> Option<V> {
79        self.buckets
80            .get(bucket)
81            .and_then(|b| b.remove(key).map(|(_, v)| v))
82    }
83
84    /// Clear all entries in a specific bucket.
85    pub fn clear_bucket(&self, bucket: &str) {
86        if let Some(b) = self.buckets.get(bucket) {
87            b.clear();
88        }
89    }
90
91    /// Clear all buckets.
92    pub fn clear_all(&self) {
93        for entry in self.buckets.iter() {
94            entry.value().clear();
95        }
96    }
97}
98
99impl<K, V> Default for Cache<K, V>
100where
101    K: Clone + Eq + Hash + Send + Sync + 'static,
102    V: Clone + Send + Sync + 'static,
103{
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_cache_basic() {
115        let cache: Cache = Cache::with_buckets(&["products", "listings"]);
116
117        cache.set("products", "upc:123".into(), "uuid-1".into());
118        assert_eq!(cache.get("products", &"upc:123".into()), Some("uuid-1".into()));
119        assert_eq!(cache.get("products", &"upc:999".into()), None);
120        assert_eq!(cache.get("listings", &"upc:123".into()), None);
121    }
122
123    #[test]
124    fn test_cache_auto_create_bucket() {
125        let cache: Cache = Cache::new();
126        cache.set("new_bucket", "key".into(), "value".into());
127        assert_eq!(cache.get("new_bucket", &"key".into()), Some("value".into()));
128    }
129
130    #[test]
131    fn test_cache_clear() {
132        let cache: Cache = Cache::with_buckets(&["a", "b"]);
133        cache.set("a", "k1".into(), "v1".into());
134        cache.set("b", "k2".into(), "v2".into());
135
136        cache.clear_bucket("a");
137        assert_eq!(cache.get("a", &"k1".into()), None);
138        assert_eq!(cache.get("b", &"k2".into()), Some("v2".into()));
139
140        cache.clear_all();
141        assert_eq!(cache.get("b", &"k2".into()), None);
142    }
143
144    #[test]
145    fn test_typed_cache_i32_values() {
146        let cache: Cache<String, i32> = Cache::with_buckets(&["counts"]);
147
148        cache.set("counts", "page_views".into(), 42);
149        cache.set("counts", "sessions".into(), 7);
150
151        assert_eq!(cache.get("counts", &"page_views".into()), Some(42));
152        assert_eq!(cache.get("counts", &"sessions".into()), Some(7));
153        assert_eq!(cache.get("counts", &"missing".into()), None);
154
155        cache.remove("counts", &"page_views".into());
156        assert_eq!(cache.get("counts", &"page_views".into()), None);
157    }
158
159    #[test]
160    fn test_typed_cache_i32_keys() {
161        let cache: Cache<i32, String> = Cache::with_buckets(&["users"]);
162
163        cache.set("users", 1, "alice".into());
164        cache.set("users", 2, "bob".into());
165
166        assert_eq!(cache.get("users", &1), Some("alice".into()));
167        assert_eq!(cache.get("users", &2), Some("bob".into()));
168        assert_eq!(cache.get("users", &3), None);
169    }
170
171    #[test]
172    fn test_typed_cache_both_generic() {
173        let cache: Cache<i32, f64> = Cache::new();
174
175        cache.set("scores", 100, 9.5);
176        cache.set("scores", 200, 8.3);
177
178        assert_eq!(cache.get("scores", &100), Some(9.5));
179        assert_eq!(cache.get("scores", &200), Some(8.3));
180    }
181
182    #[test]
183    fn test_typed_cache_vec() {
184        let cache: Cache<String, Vec<String>> = Cache::new();
185
186        cache.set(
187            "tags",
188            "item:1".into(),
189            vec!["rust".into(), "database".into()],
190        );
191
192        let tags = cache.get("tags", &"item:1".into()).unwrap();
193        assert_eq!(tags, vec!["rust".to_string(), "database".to_string()]);
194    }
195
196    #[test]
197    fn test_typed_cache_struct() {
198        #[derive(Debug, Clone, PartialEq)]
199        struct Product {
200            id: i32,
201            name: String,
202        }
203
204        let cache: Cache<String, Product> = Cache::new();
205        cache.set(
206            "products",
207            "sku:abc".into(),
208            Product {
209                id: 1,
210                name: "Widget".into(),
211            },
212        );
213
214        let product = cache.get("products", &"sku:abc".into()).unwrap();
215        assert_eq!(product.id, 1);
216        assert_eq!(product.name, "Widget");
217    }
218}