/ ferris-proof-core / src / cache_manager.rs
cache_manager.rs
  1  use crate::cache::{CompactionResult, VerificationCache};
  2  use anyhow::Result;
  3  use serde::{Deserialize, Serialize};
  4  use std::path::PathBuf;
  5  
  6  /// Cache management operations for CLI and programmatic use
  7  pub struct CacheManager {
  8      cache: VerificationCache,
  9  }
 10  
 11  #[derive(Debug, Clone, Serialize, Deserialize)]
 12  pub struct CacheInfo {
 13      pub cache_dir: PathBuf,
 14      pub total_entries: usize,
 15      pub valid_entries: usize,
 16      pub expired_entries: usize,
 17      pub total_size_bytes: u64,
 18      pub disk_size_bytes: u64,
 19  }
 20  
 21  #[derive(Debug, Clone, Serialize, Deserialize)]
 22  pub struct CacheHealthReport {
 23      pub info: CacheInfo,
 24      pub integrity_errors: Vec<String>,
 25      pub recommendations: Vec<String>,
 26  }
 27  
 28  impl CacheManager {
 29      /// Create a new cache manager with default cache directory
 30      pub fn new() -> Self {
 31          Self {
 32              cache: VerificationCache::new(),
 33          }
 34      }
 35  
 36      /// Create a new cache manager with custom cache directory
 37      pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
 38          Self {
 39              cache: VerificationCache::with_cache_dir(cache_dir),
 40          }
 41      }
 42  
 43      /// Get comprehensive cache information
 44      pub fn info(&self) -> Result<CacheInfo> {
 45          let stats = self.cache.statistics();
 46          let disk_size = self.cache.disk_size().unwrap_or(0);
 47  
 48          Ok(CacheInfo {
 49              cache_dir: stats.cache_dir,
 50              total_entries: stats.total_entries,
 51              valid_entries: stats.valid_entries,
 52              expired_entries: stats.expired_entries,
 53              total_size_bytes: stats.total_size_bytes,
 54              disk_size_bytes: disk_size,
 55          })
 56      }
 57  
 58      /// Clean up expired cache entries
 59      pub fn cleanup(&mut self) -> Result<CleanupResult> {
 60          let initial_info = self.info()?;
 61          let expired_removed = self.cache.cleanup_expired()?;
 62          let final_info = self.info()?;
 63  
 64          Ok(CleanupResult {
 65              entries_removed: expired_removed,
 66              size_freed: initial_info
 67                  .disk_size_bytes
 68                  .saturating_sub(final_info.disk_size_bytes),
 69              entries_before: initial_info.total_entries,
 70              entries_after: final_info.total_entries,
 71          })
 72      }
 73  
 74      /// Clear all cache entries
 75      pub fn clear(&mut self) -> Result<ClearResult> {
 76          let initial_info = self.info()?;
 77          self.cache.clear();
 78  
 79          Ok(ClearResult {
 80              entries_removed: initial_info.total_entries,
 81              size_freed: initial_info.disk_size_bytes,
 82          })
 83      }
 84  
 85      /// Compact cache by removing expired entries and optimizing storage
 86      pub fn compact(&mut self) -> Result<CompactionResult> {
 87          self.cache.compact()
 88      }
 89  
 90      /// Validate cache integrity and provide health report
 91      pub fn health_check(&self) -> Result<CacheHealthReport> {
 92          let info = self.info()?;
 93          let integrity_errors = self.cache.validate_integrity()?;
 94          let mut recommendations = Vec::new();
 95  
 96          // Generate recommendations based on cache state
 97          if info.expired_entries > 0 {
 98              recommendations.push(format!(
 99                  "Consider running cleanup to remove {} expired entries",
100                  info.expired_entries
101              ));
102          }
103  
104          if info.disk_size_bytes > 1_000_000_000 {
105              // > 1GB
106              recommendations.push(
107                  "Cache size is large (>1GB). Consider running compact to optimize storage"
108                      .to_string(),
109              );
110          }
111  
112          if !integrity_errors.is_empty() {
113              recommendations.push(
114                  "Cache integrity issues detected. Consider clearing and rebuilding cache"
115                      .to_string(),
116              );
117          }
118  
119          if info.total_entries == 0 {
120              recommendations.push("Cache is empty. No action needed".to_string());
121          }
122  
123          Ok(CacheHealthReport {
124              info,
125              integrity_errors,
126              recommendations,
127          })
128      }
129  
130      /// Repair cache by removing corrupted entries
131      pub fn repair(&mut self) -> Result<RepairResult> {
132          let initial_info = self.info()?;
133          let integrity_errors = self.cache.validate_integrity()?;
134  
135          if integrity_errors.is_empty() {
136              return Ok(RepairResult {
137                  corrupted_entries_removed: 0,
138                  entries_before: initial_info.total_entries,
139                  entries_after: initial_info.total_entries,
140                  size_freed: 0,
141              });
142          }
143  
144          // For now, we'll clear the entire cache if there are integrity issues
145          // In a more sophisticated implementation, we could selectively remove corrupted entries
146          let clear_result = self.clear()?;
147  
148          Ok(RepairResult {
149              corrupted_entries_removed: clear_result.entries_removed,
150              entries_before: initial_info.total_entries,
151              entries_after: 0,
152              size_freed: clear_result.size_freed,
153          })
154      }
155  
156      /// Get cache statistics for monitoring
157      pub fn statistics(&self) -> CacheStatistics {
158          let stats = self.cache.statistics();
159          let disk_size = self.cache.disk_size().unwrap_or(0);
160  
161          CacheStatistics {
162              total_entries: stats.total_entries,
163              valid_entries: stats.valid_entries,
164              expired_entries: stats.expired_entries,
165              memory_size_bytes: stats.total_size_bytes,
166              disk_size_bytes: disk_size,
167              cache_dir: stats.cache_dir,
168          }
169      }
170  
171      /// Load cache from disk
172      pub fn load(&mut self) -> Result<()> {
173          self.cache.load_from_disk()
174      }
175  
176      /// Save cache to disk
177      pub fn save(&self) -> Result<()> {
178          self.cache.save_to_disk()
179      }
180  
181      /// Get the underlying cache for advanced operations
182      pub fn cache(&self) -> &VerificationCache {
183          &self.cache
184      }
185  
186      /// Get mutable access to the underlying cache
187      pub fn cache_mut(&mut self) -> &mut VerificationCache {
188          &mut self.cache
189      }
190  }
191  
192  #[derive(Debug, Clone, Serialize, Deserialize)]
193  pub struct CleanupResult {
194      pub entries_removed: usize,
195      pub size_freed: u64,
196      pub entries_before: usize,
197      pub entries_after: usize,
198  }
199  
200  #[derive(Debug, Clone, Serialize, Deserialize)]
201  pub struct ClearResult {
202      pub entries_removed: usize,
203      pub size_freed: u64,
204  }
205  
206  #[derive(Debug, Clone, Serialize, Deserialize)]
207  pub struct RepairResult {
208      pub corrupted_entries_removed: usize,
209      pub entries_before: usize,
210      pub entries_after: usize,
211      pub size_freed: u64,
212  }
213  
214  #[derive(Debug, Clone, Serialize, Deserialize)]
215  pub struct CacheStatistics {
216      pub total_entries: usize,
217      pub valid_entries: usize,
218      pub expired_entries: usize,
219      pub memory_size_bytes: u64,
220      pub disk_size_bytes: u64,
221      pub cache_dir: PathBuf,
222  }
223  
224  impl Default for CacheManager {
225      fn default() -> Self {
226          Self::new()
227      }
228  }
229  
230  #[cfg(test)]
231  mod tests {
232      use super::*;
233      use crate::cache::{
234          CacheEntry, CacheKey, CacheMetadata, ConfigHash, ContentHash, ToolVersions,
235      };
236      use crate::types::{Layer, LayerResult, Status};
237      use std::time::Duration;
238      use tempfile::TempDir;
239  
240      #[test]
241      fn test_cache_manager_info() {
242          let temp_dir = TempDir::new().unwrap();
243          let cache_dir = temp_dir.path().join("cache");
244  
245          let manager = CacheManager::with_cache_dir(cache_dir.clone());
246          let info = manager.info().unwrap();
247  
248          assert_eq!(info.cache_dir, cache_dir);
249          assert_eq!(info.total_entries, 0);
250          assert_eq!(info.valid_entries, 0);
251          assert_eq!(info.expired_entries, 0);
252      }
253  
254      #[test]
255      fn test_cache_manager_cleanup() {
256          let temp_dir = TempDir::new().unwrap();
257          let cache_dir = temp_dir.path().join("cache");
258  
259          let mut manager = CacheManager::with_cache_dir(cache_dir);
260  
261          // Add some test entries (some expired)
262          let cache_key = CacheKey {
263              content_hash: ContentHash("test_hash".to_string()),
264              config_hash: ConfigHash("config_hash".to_string()),
265              tool_versions: ToolVersions {
266                  ferris_proof: "0.1.0".to_string(),
267                  external_tools: vec![],
268              },
269              layer: Layer::PropertyBased,
270          };
271  
272          let expired_entry = CacheEntry {
273              result: LayerResult {
274                  layer: Layer::PropertyBased,
275                  status: Status::Success,
276                  violations: vec![],
277                  execution_time: Duration::from_millis(100),
278                  tool_outputs: vec![],
279              },
280              timestamp: chrono::Utc::now() - chrono::Duration::seconds(10),
281              ttl: Duration::from_secs(5), // Expired
282              metadata: CacheMetadata {
283                  file_size: 1024,
284                  execution_time: Duration::from_millis(100),
285                  memory_usage: 512 * 1024 * 1024,
286                  cache_hit_count: 0,
287              },
288          };
289  
290          manager.cache_mut().store(cache_key, expired_entry);
291  
292          let cleanup_result = manager.cleanup().unwrap();
293          assert_eq!(cleanup_result.entries_removed, 1);
294          assert_eq!(cleanup_result.entries_before, 1);
295          assert_eq!(cleanup_result.entries_after, 0);
296      }
297  
298      #[test]
299      fn test_cache_manager_clear() {
300          let temp_dir = TempDir::new().unwrap();
301          let cache_dir = temp_dir.path().join("cache");
302  
303          let mut manager = CacheManager::with_cache_dir(cache_dir);
304  
305          // Add a test entry
306          let cache_key = CacheKey {
307              content_hash: ContentHash("test_hash".to_string()),
308              config_hash: ConfigHash("config_hash".to_string()),
309              tool_versions: ToolVersions {
310                  ferris_proof: "0.1.0".to_string(),
311                  external_tools: vec![],
312              },
313              layer: Layer::PropertyBased,
314          };
315  
316          let cache_entry = CacheEntry {
317              result: LayerResult {
318                  layer: Layer::PropertyBased,
319                  status: Status::Success,
320                  violations: vec![],
321                  execution_time: Duration::from_millis(100),
322                  tool_outputs: vec![],
323              },
324              timestamp: chrono::Utc::now(),
325              ttl: Duration::from_secs(3600),
326              metadata: CacheMetadata {
327                  file_size: 1024,
328                  execution_time: Duration::from_millis(100),
329                  memory_usage: 512 * 1024 * 1024,
330                  cache_hit_count: 0,
331              },
332          };
333  
334          manager.cache_mut().store(cache_key, cache_entry);
335  
336          let clear_result = manager.clear().unwrap();
337          assert_eq!(clear_result.entries_removed, 1);
338  
339          let info_after = manager.info().unwrap();
340          assert_eq!(info_after.total_entries, 0);
341      }
342  
343      #[test]
344      fn test_cache_manager_health_check() {
345          let temp_dir = TempDir::new().unwrap();
346          let cache_dir = temp_dir.path().join("cache");
347  
348          let manager = CacheManager::with_cache_dir(cache_dir);
349          let health_report = manager.health_check().unwrap();
350  
351          assert!(health_report.integrity_errors.is_empty());
352          assert!(!health_report.recommendations.is_empty());
353          assert!(health_report.recommendations[0].contains("Cache is empty"));
354      }
355  }