reddb_server/storage/query/planner/
cache.rs1use std::collections::HashMap;
13use std::time::{Duration, Instant};
14
15use super::{CacheStats, QueryPlan};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum CacheEntryState {
19 Inactive,
20 Active,
21}
22
23#[derive(Debug, Clone)]
25pub struct CachedPlan {
26 pub plan: QueryPlan,
28 pub cached_at: Instant,
30 pub access_count: u64,
32 pub last_accessed: Instant,
34 pub shape_key: Option<String>,
36 pub exact_query: Option<std::sync::Arc<str>>,
38 pub state: CacheEntryState,
40 pub expected_rows_scanned: Option<u64>,
42 pub last_observed_rows_scanned: Option<u64>,
44 pub parameter_count: usize,
46 pub replan_pending: bool,
48 lru_seq: u64,
53}
54
55impl CachedPlan {
56 pub fn new(plan: QueryPlan) -> Self {
58 let now = Instant::now();
59 Self {
60 plan,
61 cached_at: now,
62 access_count: 0,
63 last_accessed: now,
64 shape_key: None,
65 exact_query: None,
66 state: CacheEntryState::Inactive,
67 expected_rows_scanned: None,
68 last_observed_rows_scanned: None,
69 parameter_count: 0,
70 replan_pending: false,
71 lru_seq: 0,
72 }
73 }
74
75 pub fn with_shape_key(mut self, shape_key: impl Into<String>) -> Self {
76 self.shape_key = Some(shape_key.into());
77 self
78 }
79
80 pub fn with_exact_query(mut self, query: impl Into<String>) -> Self {
81 let s: String = query.into();
82 self.exact_query = Some(std::sync::Arc::<str>::from(s));
83 self
84 }
85
86 pub fn with_parameter_count(mut self, parameter_count: usize) -> Self {
87 self.parameter_count = parameter_count;
88 self
89 }
90
91 pub fn is_expired(&self, ttl: Duration) -> bool {
93 self.cached_at.elapsed() > ttl
94 }
95
96 pub fn touch(&mut self) {
98 self.access_count += 1;
99 self.last_accessed = Instant::now();
100 }
101
102 pub fn matches_exact_query(&self, query: &str) -> bool {
103 self.exact_query.as_deref() == Some(query)
104 }
105
106 pub fn needs_replan(&self) -> bool {
107 self.replan_pending
108 }
109
110 pub fn record_observation(&mut self, rows_scanned: u64) {
111 self.last_observed_rows_scanned = Some(rows_scanned);
112 match (self.state, self.expected_rows_scanned) {
113 (_, None) => {
114 self.expected_rows_scanned = Some(rows_scanned.max(1));
115 self.replan_pending = false;
116 }
117 (CacheEntryState::Inactive, Some(expected)) => {
118 if rows_scanned <= expected {
119 self.state = CacheEntryState::Active;
120 self.expected_rows_scanned = Some(rows_scanned.max(1));
121 self.replan_pending = false;
122 } else {
123 self.expected_rows_scanned = Some(rows_scanned.min(expected.saturating_mul(2)));
124 }
125 }
126 (CacheEntryState::Active, Some(expected)) => {
127 if rows_scanned > expected.saturating_mul(10).max(10) {
128 self.state = CacheEntryState::Inactive;
129 self.expected_rows_scanned = Some(rows_scanned.max(1));
130 self.replan_pending = true;
131 } else if rows_scanned < expected {
132 self.expected_rows_scanned = Some(rows_scanned.max(1));
133 self.replan_pending = false;
134 }
135 }
136 }
137 }
138}
139
140pub struct PlanCache {
149 entries: HashMap<String, CachedPlan>,
151 clock: u64,
153 capacity: usize,
155 ttl: Duration,
157 hits: u64,
159 misses: u64,
160}
161
162impl PlanCache {
163 pub fn new(capacity: usize) -> Self {
169 debug_assert!(capacity > 0, "plan cache capacity must be positive");
170 Self {
171 entries: HashMap::with_capacity(capacity),
172 clock: 0,
173 capacity,
174 ttl: Duration::from_secs(3600), hits: 0,
176 misses: 0,
177 }
178 }
179
180 fn next_seq(&mut self) -> u64 {
182 self.clock += 1;
183 self.clock
184 }
185
186 pub fn with_ttl(mut self, ttl: Duration) -> Self {
188 self.ttl = ttl;
189 self
190 }
191
192 pub fn peek(&self, key: &str) -> Option<&CachedPlan> {
200 let entry = self.entries.get(key)?;
201 if entry.needs_replan() || entry.is_expired(self.ttl) {
202 return None;
203 }
204 Some(entry)
205 }
206
207 pub fn get(&mut self, key: &str) -> Option<&CachedPlan> {
209 if self
210 .entries
211 .get(key)
212 .is_some_and(|entry| entry.needs_replan())
213 {
214 self.remove(key);
215 self.misses += 1;
216 return None;
217 }
218
219 let expired = match self.entries.get(key) {
221 Some(entry) => entry.is_expired(self.ttl),
222 None => {
223 self.misses += 1;
224 return None;
225 }
226 };
227 if expired {
228 self.remove(key);
230 self.misses += 1;
231 return None;
232 }
233
234 let seq = self.next_seq();
236 if let Some(entry) = self.entries.get_mut(key) {
237 entry.touch();
238 entry.lru_seq = seq;
239 }
240 self.hits += 1;
241 self.entries.get(key)
242 }
243
244 pub fn insert(&mut self, key: String, mut plan: CachedPlan) {
246 if self.entries.contains_key(&key) {
248 self.remove(&key);
249 }
250
251 while self.entries.len() >= self.capacity && !self.entries.is_empty() {
253 self.evict_lru();
254 }
255
256 plan.lru_seq = self.next_seq();
258 self.entries.insert(key, plan);
259 }
260
261 pub fn remove(&mut self, key: &str) -> Option<CachedPlan> {
263 self.entries.remove(key)
264 }
265
266 pub fn invalidate<F>(&mut self, predicate: F)
268 where
269 F: Fn(&str) -> bool,
270 {
271 let keys_to_remove: Vec<String> = self
272 .entries
273 .keys()
274 .filter(|k| predicate(k))
275 .cloned()
276 .collect();
277
278 for key in keys_to_remove {
279 self.remove(&key);
280 }
281 }
282
283 pub fn clear(&mut self) {
285 self.entries.clear();
286 }
287
288 pub fn stats(&self) -> CacheStats {
290 CacheStats {
291 hits: self.hits,
292 misses: self.misses,
293 size: self.entries.len(),
294 capacity: self.capacity,
295 }
296 }
297
298 fn evict_lru(&mut self) {
300 let victim = self
301 .entries
302 .iter()
303 .min_by_key(|(_, entry)| entry.lru_seq)
304 .map(|(key, _)| key.clone());
305 if let Some(key) = victim {
306 self.remove(&key);
307 }
308 }
309
310 pub fn prune_expired(&mut self) {
312 let expired: Vec<String> = self
313 .entries
314 .iter()
315 .filter(|(_, v)| v.is_expired(self.ttl))
316 .map(|(k, _)| k.clone())
317 .collect();
318
319 for key in expired {
320 self.remove(&key);
321 }
322 }
323
324 pub fn record_observation(&mut self, key: &str, rows_scanned: u64) {
325 if let Some(entry) = self.entries.get_mut(key) {
326 entry.record_observation(rows_scanned);
327 }
328 }
329}
330
331impl Default for PlanCache {
332 fn default() -> Self {
333 Self::new(1000)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::storage::query::ast::{Projection, QueryExpr, TableQuery};
341 use crate::storage::query::planner::cost::PlanCost;
342
343 fn make_test_plan() -> QueryPlan {
344 QueryPlan::new(
345 QueryExpr::Table(TableQuery {
346 table: "test".to_string(),
347 source: None,
348 alias: None,
349 select_items: Vec::new(),
350 columns: vec![Projection::All],
351 where_expr: None,
352 filter: None,
353 group_by_exprs: Vec::new(),
354 group_by: Vec::new(),
355 having_expr: None,
356 having: None,
357 order_by: vec![],
358 limit: None,
359 limit_param: None,
360 offset: None,
361 offset_param: None,
362 expand: None,
363 as_of: None,
364 sessionize: None,
365 distinct: false,
366 }),
367 QueryExpr::Table(TableQuery {
368 table: "test".to_string(),
369 source: None,
370 alias: None,
371 select_items: Vec::new(),
372 columns: vec![Projection::All],
373 where_expr: None,
374 filter: None,
375 group_by_exprs: Vec::new(),
376 group_by: Vec::new(),
377 having_expr: None,
378 having: None,
379 order_by: vec![],
380 limit: None,
381 limit_param: None,
382 offset: None,
383 offset_param: None,
384 expand: None,
385 as_of: None,
386 sessionize: None,
387 distinct: false,
388 }),
389 PlanCost::default(),
390 )
391 }
392
393 #[test]
394 fn test_cache_insert_and_get() {
395 let mut cache = PlanCache::new(10);
396 let plan = CachedPlan::new(make_test_plan());
397
398 cache.insert("query1".to_string(), plan);
399 assert!(cache.get("query1").is_some());
400 assert!(cache.get("query2").is_none());
401 }
402
403 #[test]
404 fn test_cache_lru_eviction() {
405 let mut cache = PlanCache::new(2);
406
407 cache.insert("q1".to_string(), CachedPlan::new(make_test_plan()));
408 cache.insert("q2".to_string(), CachedPlan::new(make_test_plan()));
409
410 let _ = cache.get("q1");
412
413 cache.insert("q3".to_string(), CachedPlan::new(make_test_plan()));
415
416 assert!(cache.get("q1").is_some());
417 assert!(cache.get("q2").is_none()); assert!(cache.get("q3").is_some());
419 }
420
421 #[test]
422 fn test_cache_stats() {
423 let mut cache = PlanCache::new(10);
424 cache.insert("q1".to_string(), CachedPlan::new(make_test_plan()));
425
426 let _ = cache.get("q1"); let _ = cache.get("q2"); let _ = cache.get("q1"); let stats = cache.stats();
431 assert_eq!(stats.hits, 2);
432 assert_eq!(stats.misses, 1);
433 }
434
435 #[test]
436 fn test_cache_invalidation() {
437 let mut cache = PlanCache::new(10);
438 cache.insert(
439 "hosts_query1".to_string(),
440 CachedPlan::new(make_test_plan()),
441 );
442 cache.insert(
443 "hosts_query2".to_string(),
444 CachedPlan::new(make_test_plan()),
445 );
446 cache.insert("users_query".to_string(), CachedPlan::new(make_test_plan()));
447
448 cache.invalidate(|k| k.starts_with("hosts_"));
450
451 assert!(cache.get("hosts_query1").is_none());
452 assert!(cache.get("hosts_query2").is_none());
453 assert!(cache.get("users_query").is_some());
454 }
455
456 #[test]
457 fn lru_eviction_picks_least_recently_used_victim() {
458 let mut cache = PlanCache::new(3);
463 for k in ["a", "b", "c"] {
464 cache.insert(k.to_string(), CachedPlan::new(make_test_plan()));
465 }
466 assert!(cache.get("a").is_some());
468 assert!(cache.get("c").is_some());
469
470 cache.insert("d".to_string(), CachedPlan::new(make_test_plan()));
472
473 assert!(
474 cache.peek("b").is_none(),
475 "b was least-recently-used and must be the eviction victim"
476 );
477 assert!(cache.peek("a").is_some());
478 assert!(cache.peek("c").is_some());
479 assert!(cache.peek("d").is_some());
480 }
481
482 #[test]
483 fn active_entry_forces_replan_after_large_regression() {
484 let mut cache = PlanCache::new(10);
485 cache.insert("q1".to_string(), CachedPlan::new(make_test_plan()));
486
487 cache.record_observation("q1", 10);
488 cache.record_observation("q1", 10);
489 cache.record_observation("q1", 500);
490
491 assert!(cache.get("q1").is_none());
492 }
493}