ricecoder_storage/
config_cache.rs

1//! Configuration caching layer
2//!
3//! Caches parsed configuration files to improve performance.
4//! Uses file-based cache with TTL support.
5
6use crate::cache::{CacheInvalidationStrategy, CacheManager};
7use crate::error::{StorageError, StorageResult};
8use serde_json::Value;
9use std::path::Path;
10use std::sync::Arc;
11use tracing::{debug, info};
12
13/// Configuration cache
14///
15/// Caches parsed configuration files to avoid redundant parsing.
16/// Supports both global and project-level configuration caching.
17pub struct ConfigCache {
18    cache: Arc<CacheManager>,
19    ttl_seconds: u64,
20}
21
22impl ConfigCache {
23    /// Create a new config cache
24    ///
25    /// # Arguments
26    ///
27    /// * `cache_dir` - Directory to store cache files
28    /// * `ttl_seconds` - Time-to-live for cache entries (default: 3600 = 1 hour)
29    ///
30    /// # Errors
31    ///
32    /// Returns error if cache directory cannot be created
33    pub fn new(cache_dir: impl AsRef<Path>, ttl_seconds: u64) -> StorageResult<Self> {
34        let cache = CacheManager::new(cache_dir)?;
35
36        Ok(Self {
37            cache: Arc::new(cache),
38            ttl_seconds,
39        })
40    }
41
42    /// Get a cached configuration
43    ///
44    /// # Arguments
45    ///
46    /// * `config_path` - Path to configuration file
47    ///
48    /// # Returns
49    ///
50    /// Returns cached configuration if found and not expired, None otherwise
51    pub fn get(&self, config_path: &Path) -> StorageResult<Option<Value>> {
52        let cache_key = self.make_cache_key(config_path);
53
54        match self.cache.get(&cache_key) {
55            Ok(Some(cached_json)) => {
56                match serde_json::from_str::<Value>(&cached_json) {
57                    Ok(config) => {
58                        debug!("Cache hit for config: {}", config_path.display());
59                        Ok(Some(config))
60                    }
61                    Err(e) => {
62                        debug!("Failed to deserialize cached config: {}", e);
63                        // Invalidate corrupted cache entry
64                        let _ = self.cache.invalidate(&cache_key);
65                        Ok(None)
66                    }
67                }
68            }
69            Ok(None) => {
70                debug!("Cache miss for config: {}", config_path.display());
71                Ok(None)
72            }
73            Err(e) => {
74                debug!("Cache lookup error: {}", e);
75                Ok(None)
76            }
77        }
78    }
79
80    /// Cache a configuration
81    ///
82    /// # Arguments
83    ///
84    /// * `config_path` - Path to configuration file
85    /// * `config` - Parsed configuration to cache
86    ///
87    /// # Errors
88    ///
89    /// Returns error if configuration cannot be cached
90    pub fn set(&self, config_path: &Path, config: &Value) -> StorageResult<()> {
91        let cache_key = self.make_cache_key(config_path);
92
93        let config_json = serde_json::to_string(config)
94            .map_err(|e| StorageError::internal(format!("Failed to serialize config: {}", e)))?;
95
96        let json_len = config_json.len();
97
98        self.cache.set(
99            &cache_key,
100            config_json,
101            CacheInvalidationStrategy::Ttl(self.ttl_seconds),
102        )?;
103
104        debug!(
105            "Cached config: {} ({} bytes)",
106            config_path.display(),
107            json_len
108        );
109
110        Ok(())
111    }
112
113    /// Invalidate a cached configuration
114    ///
115    /// # Arguments
116    ///
117    /// * `config_path` - Path to configuration file
118    ///
119    /// # Returns
120    ///
121    /// Returns Ok(true) if entry was deleted, Ok(false) if entry didn't exist
122    pub fn invalidate(&self, config_path: &Path) -> StorageResult<bool> {
123        let cache_key = self.make_cache_key(config_path);
124        self.cache.invalidate(&cache_key)
125    }
126
127    /// Clear all cached configurations
128    ///
129    /// # Errors
130    ///
131    /// Returns error if cache cannot be cleared
132    pub fn clear(&self) -> StorageResult<()> {
133        self.cache.clear()
134    }
135
136    /// Clean up expired cache entries
137    ///
138    /// # Returns
139    ///
140    /// Returns the number of entries cleaned up
141    pub fn cleanup_expired(&self) -> StorageResult<usize> {
142        let cleaned = self.cache.cleanup_expired()?;
143
144        if cleaned > 0 {
145            info!("Cleaned up {} expired config cache entries", cleaned);
146        }
147
148        Ok(cleaned)
149    }
150
151    /// Create a cache key from config path
152    fn make_cache_key(&self, config_path: &Path) -> String {
153        let path_str = config_path.to_string_lossy();
154        let sanitized = path_str
155            .chars()
156            .map(|c| {
157                if c.is_alphanumeric() || c == '_' || c == '-' || c == '.' {
158                    c
159                } else {
160                    '_'
161                }
162            })
163            .collect::<String>();
164
165        format!("config_{}", sanitized)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use tempfile::TempDir;
173
174    #[test]
175    fn test_cache_set_and_get() -> StorageResult<()> {
176        let temp_dir = TempDir::new().unwrap();
177        let cache = ConfigCache::new(temp_dir.path(), 3600)?;
178
179        let config_path = std::path::PathBuf::from("config.yaml");
180        let config = serde_json::json!({
181            "key": "value",
182            "nested": {
183                "setting": 42
184            }
185        });
186
187        // Cache config
188        cache.set(&config_path, &config)?;
189
190        // Retrieve from cache
191        let cached = cache.get(&config_path)?;
192        assert!(cached.is_some());
193        assert_eq!(cached.unwrap()["key"], "value");
194
195        Ok(())
196    }
197
198    #[test]
199    fn test_cache_miss() -> StorageResult<()> {
200        let temp_dir = TempDir::new().unwrap();
201        let cache = ConfigCache::new(temp_dir.path(), 3600)?;
202
203        let config_path = std::path::PathBuf::from("nonexistent.yaml");
204
205        // Try to get non-existent entry
206        let cached = cache.get(&config_path)?;
207        assert!(cached.is_none());
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_cache_invalidate() -> StorageResult<()> {
214        let temp_dir = TempDir::new().unwrap();
215        let cache = ConfigCache::new(temp_dir.path(), 3600)?;
216
217        let config_path = std::path::PathBuf::from("config.yaml");
218        let config = serde_json::json!({"key": "value"});
219
220        // Cache config
221        cache.set(&config_path, &config)?;
222
223        // Invalidate
224        let invalidated = cache.invalidate(&config_path)?;
225        assert!(invalidated);
226
227        // Should be gone now
228        let cached = cache.get(&config_path)?;
229        assert!(cached.is_none());
230
231        Ok(())
232    }
233
234    #[test]
235    fn test_cache_clear() -> StorageResult<()> {
236        let temp_dir = TempDir::new().unwrap();
237        let cache = ConfigCache::new(temp_dir.path(), 3600)?;
238
239        let config_path1 = std::path::PathBuf::from("config1.yaml");
240        let config_path2 = std::path::PathBuf::from("config2.yaml");
241        let config = serde_json::json!({"key": "value"});
242
243        // Cache multiple configs
244        cache.set(&config_path1, &config)?;
245        cache.set(&config_path2, &config)?;
246
247        // Clear all
248        cache.clear()?;
249
250        // Both should be gone
251        assert!(cache.get(&config_path1)?.is_none());
252        assert!(cache.get(&config_path2)?.is_none());
253
254        Ok(())
255    }
256}