1use std::num::NonZeroUsize;
12use std::sync::Mutex;
13
14use bytes::Bytes;
15
16pub struct QueryCache {
17 inner: Mutex<lru::LruCache<String, Bytes>>,
18}
19
20impl QueryCache {
21 pub fn new(capacity: usize) -> Self {
22 let cap = NonZeroUsize::new(capacity.max(1)).expect("capacity > 0");
23 Self {
24 inner: Mutex::new(lru::LruCache::new(cap)),
25 }
26 }
27
28 pub fn get(&self, key: &str) -> Option<Bytes> {
29 self.inner.lock().ok()?.get(key).cloned()
30 }
31
32 pub fn put(&self, key: String, value: Bytes) {
33 if let Ok(mut lru) = self.inner.lock() {
34 lru.put(key, value);
35 }
36 }
37
38 pub fn invalidate_prefix(&self, prefix: &str) {
41 let prefix = format!("{prefix}:");
42 let mut lru = match self.inner.lock() {
43 Ok(g) => g,
44 Err(_) => return,
45 };
46 let stale: Vec<String> = lru
47 .iter()
48 .filter(|(k, _)| k.starts_with(&prefix))
49 .map(|(k, _)| k.clone())
50 .collect();
51 for k in stale {
52 lru.pop(&k);
53 }
54 }
55
56 pub fn clear(&self) {
57 if let Ok(mut lru) = self.inner.lock() {
58 lru.clear();
59 }
60 }
61
62 pub fn len(&self) -> usize {
63 self.inner.lock().map(|l| l.len()).unwrap_or(0)
64 }
65
66 pub fn is_empty(&self) -> bool {
67 self.len() == 0
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn put_then_get() {
77 let c = QueryCache::new(4);
78 c.put("posts:list".into(), Bytes::from_static(b"hello"));
79 assert_eq!(c.get("posts:list"), Some(Bytes::from_static(b"hello")));
80 }
81
82 #[test]
83 fn invalidate_prefix_only_hits_table() {
84 let c = QueryCache::new(16);
85 c.put("posts:list".into(), Bytes::from_static(b"a"));
86 c.put("posts:count".into(), Bytes::from_static(b"b"));
87 c.put("users:list".into(), Bytes::from_static(b"c"));
88 c.invalidate_prefix("posts");
89 assert!(c.get("posts:list").is_none());
90 assert!(c.get("posts:count").is_none());
91 assert_eq!(c.get("users:list"), Some(Bytes::from_static(b"c")));
92 }
93}