Skip to main content

code_analyze_mcp/
cache.rs

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