Skip to main content

driven/fusion/
binary_cache.rs

1//! Persistent Binary Cache
2//!
3//! Disk-backed cache for compiled templates.
4
5use crate::Result;
6use std::collections::HashMap;
7use std::fs::{self, File};
8use std::io::{Read, Write};
9use std::path::{Path, PathBuf};
10
11/// Cache key (based on template and source hashes)
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct CacheKey {
14    /// Template hash
15    pub template_hash: u64,
16    /// Source hash
17    pub source_hash: u64,
18}
19
20impl CacheKey {
21    /// Create a new cache key
22    pub fn new(template_hash: u64, source_hash: u64) -> Self {
23        Self {
24            template_hash,
25            source_hash,
26        }
27    }
28
29    /// Generate filename for cache entry
30    pub fn filename(&self) -> String {
31        format!("{:016x}_{:016x}.dtm", self.template_hash, self.source_hash)
32    }
33
34    /// Parse from filename
35    pub fn from_filename(name: &str) -> Option<Self> {
36        let name = name.strip_suffix(".dtm")?;
37        let mut parts = name.split('_');
38
39        let template_hash = u64::from_str_radix(parts.next()?, 16).ok()?;
40        let source_hash = u64::from_str_radix(parts.next()?, 16).ok()?;
41
42        Some(Self {
43            template_hash,
44            source_hash,
45        })
46    }
47}
48
49/// Cache entry metadata
50#[derive(Debug, Clone)]
51pub struct CacheEntry {
52    /// Cache key
53    pub key: CacheKey,
54    /// File path
55    pub path: PathBuf,
56    /// Size in bytes
57    pub size: u64,
58    /// Creation timestamp
59    pub created: std::time::SystemTime,
60}
61
62/// Persistent binary cache
63#[derive(Debug)]
64pub struct BinaryCache {
65    /// Cache directory
66    cache_dir: PathBuf,
67    /// Index of cached entries
68    index: HashMap<u64, CacheEntry>,
69    /// Maximum cache size in bytes
70    max_size: u64,
71    /// Current cache size
72    current_size: u64,
73}
74
75impl BinaryCache {
76    /// Open or create a cache directory
77    pub fn open(cache_dir: &Path) -> Result<Self> {
78        fs::create_dir_all(cache_dir)?;
79
80        let mut cache = Self {
81            cache_dir: cache_dir.to_path_buf(),
82            index: HashMap::new(),
83            max_size: 512 * 1024 * 1024, // 512MB default
84            current_size: 0,
85        };
86
87        cache.scan()?;
88
89        Ok(cache)
90    }
91
92    /// Set maximum cache size
93    pub fn with_max_size(mut self, max_size: u64) -> Self {
94        self.max_size = max_size;
95        self
96    }
97
98    /// Scan cache directory for existing entries
99    fn scan(&mut self) -> Result<()> {
100        for entry in fs::read_dir(&self.cache_dir)? {
101            let entry = entry?;
102            let path = entry.path();
103
104            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
105                if let Some(key) = CacheKey::from_filename(name) {
106                    if let Ok(metadata) = entry.metadata() {
107                        let cache_entry = CacheEntry {
108                            key,
109                            path: path.clone(),
110                            size: metadata.len(),
111                            created: metadata
112                                .created()
113                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
114                        };
115                        self.current_size += cache_entry.size;
116                        self.index.insert(key.template_hash, cache_entry);
117                    }
118                }
119            }
120        }
121
122        Ok(())
123    }
124
125    /// Get cached template
126    pub fn get(&self, template_hash: u64, source_hash: u64) -> Result<Option<Vec<u8>>> {
127        if let Some(entry) = self.index.get(&template_hash) {
128            // Check if source hash matches (not stale)
129            if entry.key.source_hash == source_hash {
130                let mut file = File::open(&entry.path)?;
131                let mut data = Vec::new();
132                file.read_to_end(&mut data)?;
133                return Ok(Some(data));
134            }
135            // Stale entry - caller should recompile
136        }
137
138        Ok(None)
139    }
140
141    /// Store compiled template
142    pub fn put(&mut self, template_hash: u64, source_hash: u64, data: &[u8]) -> Result<()> {
143        let key = CacheKey::new(template_hash, source_hash);
144
145        // Evict if necessary
146        while self.current_size + data.len() as u64 > self.max_size {
147            self.evict_oldest()?;
148        }
149
150        // Remove old version if exists
151        if let Some(old) = self.index.remove(&template_hash) {
152            self.current_size -= old.size;
153            let _ = fs::remove_file(&old.path);
154        }
155
156        // Write new entry
157        let path = self.cache_dir.join(key.filename());
158        let mut file = File::create(&path)?;
159        file.write_all(data)?;
160
161        let entry = CacheEntry {
162            key,
163            path,
164            size: data.len() as u64,
165            created: std::time::SystemTime::now(),
166        };
167
168        self.current_size += entry.size;
169        self.index.insert(template_hash, entry);
170
171        Ok(())
172    }
173
174    /// Remove a cached entry
175    pub fn remove(&mut self, template_hash: u64) -> Result<bool> {
176        if let Some(entry) = self.index.remove(&template_hash) {
177            self.current_size -= entry.size;
178            fs::remove_file(&entry.path)?;
179            return Ok(true);
180        }
181        Ok(false)
182    }
183
184    /// Clear entire cache
185    pub fn clear(&mut self) -> Result<()> {
186        for entry in self.index.values() {
187            let _ = fs::remove_file(&entry.path);
188        }
189        self.index.clear();
190        self.current_size = 0;
191        Ok(())
192    }
193
194    /// Get cache statistics
195    pub fn stats(&self) -> BinaryCacheStats {
196        BinaryCacheStats {
197            entries: self.index.len(),
198            size_bytes: self.current_size,
199            max_size_bytes: self.max_size,
200        }
201    }
202
203    /// Evict oldest entry
204    fn evict_oldest(&mut self) -> Result<()> {
205        let oldest = self
206            .index
207            .iter()
208            .min_by_key(|(_, e)| e.created)
209            .map(|(k, _)| *k);
210
211        if let Some(key) = oldest {
212            self.remove(key)?;
213        }
214
215        Ok(())
216    }
217}
218
219/// Cache statistics
220#[derive(Debug, Clone)]
221pub struct BinaryCacheStats {
222    /// Number of cached entries
223    pub entries: usize,
224    /// Current cache size in bytes
225    pub size_bytes: u64,
226    /// Maximum cache size in bytes
227    pub max_size_bytes: u64,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_cache_key_filename() {
236        let key = CacheKey::new(0x1234567890ABCDEF, 0xFEDCBA0987654321);
237        let filename = key.filename();
238
239        assert_eq!(filename, "1234567890abcdef_fedcba0987654321.dtm");
240
241        let parsed = CacheKey::from_filename(&filename).unwrap();
242        assert_eq!(parsed, key);
243    }
244
245    #[test]
246    fn test_cache_operations() {
247        let temp_dir = std::env::temp_dir().join("driven_cache_test");
248        let _ = fs::remove_dir_all(&temp_dir);
249
250        let mut cache = BinaryCache::open(&temp_dir).unwrap();
251
252        // Put and get
253        cache.put(1, 100, b"test data").unwrap();
254        let data = cache.get(1, 100).unwrap();
255        assert_eq!(data.as_deref(), Some(b"test data".as_slice()));
256
257        // Stale check
258        let stale = cache.get(1, 200).unwrap();
259        assert!(stale.is_none());
260
261        // Clean up
262        let _ = fs::remove_dir_all(&temp_dir);
263    }
264}