Skip to main content

code_analyze_mcp/
cache.rs

1//! LRU cache for analysis results indexed by path, modification time, and mode.
2//!
3//! Provides thread-safe, capacity-bounded caching of file analysis outputs using LRU eviction.
4//! Recovers gracefully from poisoned mutex conditions.
5
6use crate::analyze::FileAnalysisOutput;
7use crate::types::AnalysisMode;
8use lru::LruCache;
9use std::num::NonZeroUsize;
10use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12use std::time::SystemTime;
13use tracing::{debug, instrument};
14
15/// Cache key combining path, modification time, and analysis mode.
16#[derive(Debug, Clone, Eq, PartialEq, Hash)]
17pub struct CacheKey {
18    pub path: PathBuf,
19    pub modified: SystemTime,
20    pub mode: AnalysisMode,
21}
22
23/// Recover from a poisoned mutex by clearing the cache.
24/// On poison, creates a new empty cache and returns the recovery value.
25fn lock_or_recover<T, F>(
26    mutex: &Mutex<LruCache<CacheKey, Arc<FileAnalysisOutput>>>,
27    recovery: F,
28) -> T
29where
30    F: FnOnce(&mut LruCache<CacheKey, Arc<FileAnalysisOutput>>) -> T,
31{
32    match mutex.lock() {
33        Ok(mut guard) => recovery(&mut guard),
34        Err(poisoned) => {
35            let cache_size = NonZeroUsize::new(100).unwrap();
36            let new_cache = LruCache::new(cache_size);
37            let mut guard = poisoned.into_inner();
38            *guard = new_cache;
39            recovery(&mut guard)
40        }
41    }
42}
43
44/// LRU cache for file analysis results with mutex protection.
45pub struct AnalysisCache {
46    cache: Arc<Mutex<LruCache<CacheKey, Arc<FileAnalysisOutput>>>>,
47}
48
49impl AnalysisCache {
50    /// Create a new cache with the specified capacity.
51    pub fn new(capacity: usize) -> Self {
52        let cache_size = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap());
53        Self {
54            cache: Arc::new(Mutex::new(LruCache::new(cache_size))),
55        }
56    }
57
58    /// Get a cached analysis result if it exists.
59    #[instrument(skip(self), fields(path = ?key.path))]
60    pub fn get(&self, key: &CacheKey) -> Option<Arc<FileAnalysisOutput>> {
61        lock_or_recover(&self.cache, |guard| {
62            let result = guard.get(key).cloned();
63            let cache_size = guard.len();
64            match result {
65                Some(v) => {
66                    debug!(cache_event = "hit", cache_size = cache_size, path = ?key.path);
67                    Some(v)
68                }
69                None => {
70                    debug!(cache_event = "miss", cache_size = cache_size, path = ?key.path);
71                    None
72                }
73            }
74        })
75    }
76
77    /// Store an analysis result in the cache.
78    #[instrument(skip(self, value), fields(path = ?key.path))]
79    pub fn put(&self, key: CacheKey, value: Arc<FileAnalysisOutput>) {
80        lock_or_recover(&self.cache, |guard| {
81            let push_result = guard.push(key.clone(), value);
82            let cache_size = guard.len();
83            match push_result {
84                None => {
85                    debug!(cache_event = "insert", cache_size = cache_size, path = ?key.path);
86                }
87                Some((returned_key, _)) => {
88                    if returned_key == key {
89                        debug!(cache_event = "update", cache_size = cache_size, path = ?key.path);
90                    } else {
91                        debug!(cache_event = "eviction", cache_size = cache_size, path = ?key.path, evicted_path = ?returned_key.path);
92                    }
93                }
94            }
95        });
96    }
97}
98
99impl Clone for AnalysisCache {
100    fn clone(&self) -> Self {
101        Self {
102            cache: Arc::clone(&self.cache),
103        }
104    }
105}