1use super::types::CacheStats;
54use crate::cache::CacheConfig;
55use crate::cache::policy::{
56 CacheAdmission, CachePolicy, CachePolicyConfig, CachePolicyKind, build_cache_policy,
57};
58use crate::query::ParsedQuery;
59use log::{debug, warn};
60use lru::LruCache;
61use parking_lot::RwLock;
62use std::num::NonZeroUsize;
63use std::sync::Arc;
64use std::sync::atomic::{AtomicU64, Ordering};
65
66const MIN_CAPACITY: u64 = 1;
68const AST_ENTRY_WEIGHT_BYTES: u64 = 2048;
69
70pub struct AstParseCache {
72 cache: RwLock<LruCache<String, Arc<ParsedQuery>>>,
73 capacity: usize,
74 policy: Arc<dyn CachePolicy<String>>,
75 hits: AtomicU64,
76 misses: AtomicU64,
77 evictions: AtomicU64,
78}
79
80impl AstParseCache {
81 #[must_use]
83 pub fn new(capacity: usize) -> Self {
84 let cap = if capacity == 0 {
85 warn!("AstParseCache::new received zero capacity; defaulting to {MIN_CAPACITY}");
86 #[allow(clippy::cast_possible_truncation)]
87 {
88 MIN_CAPACITY as usize
89 }
90 } else {
91 capacity
92 };
93 let (kind, window_ratio) = Self::policy_params_from_env();
94 Self::with_policy(cap, kind, window_ratio)
95 }
96
97 fn with_policy(capacity: usize, kind: CachePolicyKind, window_ratio: f32) -> Self {
98 #[allow(clippy::cast_possible_truncation)]
99 let normalized_capacity = capacity.max(MIN_CAPACITY as usize);
100 let cap = NonZeroUsize::new(normalized_capacity).unwrap_or(NonZeroUsize::MIN);
101 let policy_config = CachePolicyConfig::new(kind, normalized_capacity as u64, window_ratio);
102 Self {
103 cache: RwLock::new(LruCache::new(cap)),
104 capacity: normalized_capacity,
105 policy: build_cache_policy(&policy_config),
106 hits: AtomicU64::new(0),
107 misses: AtomicU64::new(0),
108 evictions: AtomicU64::new(0),
109 }
110 }
111
112 fn policy_params_from_env() -> (CachePolicyKind, f32) {
113 let cfg = CacheConfig::from_env();
114 (cfg.policy_kind(), cfg.policy_window_ratio())
115 }
116
117 fn handle_policy_evictions(&self) {
118 let evicted = self.policy.drain_evictions();
119 if evicted.is_empty() {
120 return;
121 }
122 let mut cache = self.cache.write();
123 for eviction in evicted {
124 if cache.pop(&eviction.key).is_some() {
125 self.evictions.fetch_add(1, Ordering::Relaxed);
126 }
127 }
128 }
129
130 pub fn get(&self, query_str: &str) -> Option<Arc<ParsedQuery>> {
132 self.handle_policy_evictions();
133 let mut cache = self.cache.write();
134 if let Some(parsed_arc) = cache.get(query_str) {
135 self.hits.fetch_add(1, Ordering::Relaxed);
136 let key = query_str.to_owned();
137 let _ = self.policy.record_hit(&key);
138 Some(parsed_arc.clone())
139 } else {
140 self.misses.fetch_add(1, Ordering::Relaxed);
141 None
142 }
143 }
144
145 pub fn insert(&self, query_str: String, parsed: ParsedQuery) {
147 self.insert_arc(query_str, Arc::new(parsed));
148 }
149
150 pub fn insert_arc(&self, query_str: String, parsed_arc: Arc<ParsedQuery>) {
152 self.handle_policy_evictions();
153
154 let key_clone = query_str.clone();
155 if matches!(
156 self.policy.admit(&key_clone, AST_ENTRY_WEIGHT_BYTES),
157 CacheAdmission::Rejected
158 ) {
159 debug!(
160 "AST parse cache policy {:?} rejected entry",
161 self.policy.kind()
162 );
163 return;
164 }
165
166 let mut cache = self.cache.write();
167 if cache.len() == self.capacity
168 && let Some((evicted_key, _)) = cache.pop_lru()
169 {
170 self.policy.invalidate(&evicted_key);
171 self.evictions.fetch_add(1, Ordering::Relaxed);
172 }
173 cache.put(query_str, parsed_arc);
174 }
175
176 pub fn stats(&self) -> CacheStats {
178 CacheStats {
179 hits: self.hits.load(Ordering::Relaxed),
180 misses: self.misses.load(Ordering::Relaxed),
181 evictions: self.evictions.load(Ordering::Relaxed),
182 }
183 }
184
185 pub fn clear(&self) {
187 let mut cache = self.cache.write();
188 cache.clear();
189 self.hits.store(0, Ordering::Relaxed);
190 self.misses.store(0, Ordering::Relaxed);
191 self.evictions.store(0, Ordering::Relaxed);
192 self.policy.reset();
193 }
194
195 pub fn len(&self) -> usize {
197 self.cache.read().len()
198 }
199
200 pub fn is_empty(&self) -> bool {
202 self.len() == 0
203 }
204
205 #[cfg(test)]
206 fn with_policy_kind(capacity: usize, kind: CachePolicyKind) -> Self {
207 Self::with_policy(capacity, kind, CacheConfig::DEFAULT_POLICY_WINDOW_RATIO)
208 }
209
210 #[cfg(test)]
211 fn policy_metrics(&self) -> crate::cache::policy::CachePolicyMetrics {
212 self.policy.stats()
213 }
214}
215
216#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::cache::policy::CachePolicyKind;
224 use crate::query::types::{Condition, Expr, Field, Operator, Query as QueryAST, Span, Value};
225
226 fn make_test_parsed_query(_query_str: &str) -> ParsedQuery {
227 let ast = QueryAST {
229 root: Expr::Condition(Condition {
230 field: Field::new("kind"),
231 operator: Operator::Equal,
232 value: Value::String("function".to_string()),
233 span: Span::new(0, 13),
234 }),
235 span: Span::new(0, 13),
236 };
237
238 ParsedQuery::from_ast(Arc::new(ast)).unwrap()
239 }
240
241 #[test]
242 fn ast_parse_cache_hit() {
243 let cache = AstParseCache::new(100);
244 let query_str = "kind:function";
245 let parsed = make_test_parsed_query(query_str);
246
247 cache.insert(query_str.to_string(), parsed.clone());
249
250 let cached = cache.get(query_str).unwrap();
252 assert_eq!(cached.normalized.as_ref(), parsed.normalized.as_ref());
253
254 let stats = cache.stats();
256 assert_eq!(stats.hits, 1);
257 assert_eq!(stats.misses, 0);
258 }
259
260 #[test]
261 fn ast_parse_cache_miss() {
262 let cache = AstParseCache::new(100);
263
264 let result = cache.get("kind:function");
266 assert!(result.is_none());
267
268 let stats = cache.stats();
270 assert_eq!(stats.hits, 0);
271 assert_eq!(stats.misses, 1);
272 }
273
274 #[test]
275 fn ast_parse_cache_eviction() {
276 let cache = AstParseCache::new(2);
277
278 cache.insert("q1".into(), make_test_parsed_query("q1"));
279 cache.insert("q2".into(), make_test_parsed_query("q2"));
280 cache.insert("q3".into(), make_test_parsed_query("q3"));
281
282 let count = cache.len();
284 assert!(
285 count <= 2,
286 "Cache should have at most 2 entries after eviction, got {count}"
287 );
288
289 let stats = cache.stats();
291 assert!(
292 stats.evictions >= 1,
293 "Eviction counter should be incremented (got {})",
294 stats.evictions
295 );
296 }
297
298 #[test]
299 fn ast_parse_cache_clear() {
300 let cache = AstParseCache::new(100);
301 cache.insert("q1".into(), make_test_parsed_query("q1"));
302
303 cache.clear();
304
305 assert_eq!(cache.len(), 0);
306 assert!(cache.get("q1").is_none());
307 }
308
309 #[test]
310 fn ast_parse_cache_zero_capacity_defaults_to_one() {
311 let cache = AstParseCache::new(0);
312
313 cache.insert("q1".into(), make_test_parsed_query("q1"));
314 cache.insert("q2".into(), make_test_parsed_query("q2"));
315
316 let count = cache.len();
318 assert!(
319 count <= 1,
320 "Cache with capacity 1 should have at most 1 entry, got {count}"
321 );
322 }
323
324 #[test]
325 fn ast_parse_cache_arc_sharing() {
326 let cache = AstParseCache::new(100);
327 let query_str = "kind:function";
328 let parsed = make_test_parsed_query(query_str);
329
330 cache.insert(query_str.to_string(), parsed);
332
333 let cached1 = cache.get(query_str).unwrap();
335 let cached2 = cache.get(query_str).unwrap();
336
337 assert!(Arc::ptr_eq(&cached1, &cached2));
339 }
340
341 #[test]
342 fn tiny_lfu_preserves_hot_queries() {
343 let cache = AstParseCache::with_policy_kind(3, CachePolicyKind::TinyLfu);
344 cache.insert("hot".into(), make_test_parsed_query("hot"));
345
346 for i in 0..20 {
347 let query = format!("cold{i}");
348 cache.insert(query.clone(), make_test_parsed_query(&query));
349 }
350
351 let metrics = cache.policy_metrics();
352 assert!(
353 metrics.lfu_rejects > 0,
354 "expected TinyLFU to reject cold entries"
355 );
356 }
357}