1use std::{
7 num::NonZeroUsize,
8 sync::{Arc, Mutex},
9};
10
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13
14use crate::{error::Result, schema::CompiledSchema};
15
16#[derive(Debug, Clone)]
18pub struct CachedCompilation {
19 pub schema: Arc<CompiledSchema>,
21
22 pub hit_count: u64,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct CompilationCacheConfig {
29 pub enabled: bool,
31
32 pub max_entries: usize,
34}
35
36impl Default for CompilationCacheConfig {
37 fn default() -> Self {
38 Self {
39 enabled: true,
40 max_entries: 100,
41 }
42 }
43}
44
45impl CompilationCacheConfig {
46 #[must_use]
50 pub const fn disabled() -> Self {
51 Self {
52 enabled: false,
53 max_entries: 0,
54 }
55 }
56}
57
58pub struct CompilationCache {
93 cache: Arc<Mutex<lru::LruCache<String, CachedCompilation>>>,
95
96 config: CompilationCacheConfig,
98
99 metrics: Arc<Mutex<CompilationCacheMetrics>>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct CompilationCacheMetrics {
106 pub hits: u64,
108
109 pub misses: u64,
111
112 pub total_compilations: u64,
114
115 pub size: usize,
117}
118
119impl CompilationCache {
120 #[must_use]
126 pub fn new(config: CompilationCacheConfig) -> Self {
127 if config.enabled {
128 let max = NonZeroUsize::new(config.max_entries)
129 .expect("max_entries must be > 0 when cache is enabled");
130 Self {
131 cache: Arc::new(Mutex::new(lru::LruCache::new(max))),
132 config,
133 metrics: Arc::new(Mutex::new(CompilationCacheMetrics {
134 hits: 0,
135 misses: 0,
136 total_compilations: 0,
137 size: 0,
138 })),
139 }
140 } else {
141 let max = NonZeroUsize::new(1).expect("impossible");
143 Self {
144 cache: Arc::new(Mutex::new(lru::LruCache::new(max))),
145 config,
146 metrics: Arc::new(Mutex::new(CompilationCacheMetrics {
147 hits: 0,
148 misses: 0,
149 total_compilations: 0,
150 size: 0,
151 })),
152 }
153 }
154 }
155
156 fn fingerprint(schema_json: &str) -> String {
160 let mut hasher = Sha256::new();
161 hasher.update(schema_json.as_bytes());
162 format!("{:x}", hasher.finalize())
163 }
164
165 pub fn compile(
180 &self,
181 compiler: &crate::compiler::Compiler,
182 schema_json: &str,
183 ) -> Result<Arc<CompiledSchema>> {
184 if !self.config.enabled {
185 let schema = Arc::new(compiler.compile(schema_json)?);
187
188 let mut metrics = self.metrics.lock().expect("metrics lock poisoned");
189 metrics.total_compilations += 1;
190 metrics.misses += 1;
191
192 return Ok(schema);
193 }
194
195 let fingerprint = Self::fingerprint(schema_json);
196
197 {
199 let mut cache = self.cache.lock().expect("cache lock poisoned");
200 if let Some(cached) = cache.get_mut(&fingerprint) {
201 let mut metrics = self.metrics.lock().expect("metrics lock poisoned");
203 metrics.hits += 1;
204 cached.hit_count += 1;
205 return Ok(Arc::clone(&cached.schema));
206 }
207 }
208
209 let schema = Arc::new(compiler.compile(schema_json)?);
211
212 {
214 let mut cache = self.cache.lock().expect("cache lock poisoned");
215 cache.put(
216 fingerprint,
217 CachedCompilation {
218 schema: Arc::clone(&schema),
219 hit_count: 0,
220 },
221 );
222
223 let mut metrics = self.metrics.lock().expect("metrics lock poisoned");
224 metrics.total_compilations += 1;
225 metrics.misses += 1;
226 metrics.size = cache.len();
227 }
228
229 Ok(schema)
230 }
231
232 pub fn metrics(&self) -> Result<CompilationCacheMetrics> {
234 let metrics = self.metrics.lock().expect("metrics lock poisoned");
235 Ok(metrics.clone())
236 }
237
238 pub fn clear(&self) -> Result<()> {
240 self.cache.lock().expect("cache lock poisoned").clear();
241 let mut metrics = self.metrics.lock().expect("metrics lock poisoned");
242 metrics.size = 0;
243 Ok(())
244 }
245
246 pub fn hit_rate(&self) -> Result<f64> {
248 let metrics = self.metrics.lock().expect("metrics lock poisoned");
249 if metrics.total_compilations == 0 {
250 return Ok(0.0);
251 }
252 Ok((metrics.hits as f64 / metrics.total_compilations as f64) * 100.0)
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_fingerprint_deterministic() {
262 let schema = r#"{"types": [], "queries": []}"#;
263 let fp1 = CompilationCache::fingerprint(schema);
264 let fp2 = CompilationCache::fingerprint(schema);
265 assert_eq!(fp1, fp2, "Fingerprints should be deterministic");
266 }
267
268 #[test]
269 fn test_fingerprint_unique() {
270 let schema1 = r#"{"types": [], "queries": []}"#;
271 let schema2 = r#"{"types": [{"name": "User"}], "queries": []}"#;
272 let fp1 = CompilationCache::fingerprint(schema1);
273 let fp2 = CompilationCache::fingerprint(schema2);
274 assert_ne!(fp1, fp2, "Different schemas should have different fingerprints");
275 }
276
277 #[test]
278 fn test_cache_new_enabled() {
279 let config = CompilationCacheConfig {
280 enabled: true,
281 max_entries: 50,
282 };
283 let cache = CompilationCache::new(config);
284 assert!(cache.config.enabled);
285 }
286
287 #[test]
288 fn test_cache_new_disabled() {
289 let config = CompilationCacheConfig::disabled();
290 let cache = CompilationCache::new(config);
291 assert!(!cache.config.enabled);
292 }
293
294 #[test]
295 fn test_cache_default_config() {
296 let config = CompilationCacheConfig::default();
297 assert!(config.enabled);
298 assert_eq!(config.max_entries, 100);
299 }
300
301 #[test]
302 fn test_metrics_initial_state() {
303 let cache = CompilationCache::new(CompilationCacheConfig::default());
304 let metrics = cache.metrics().expect("metrics should work");
305 assert_eq!(metrics.hits, 0);
306 assert_eq!(metrics.misses, 0);
307 assert_eq!(metrics.total_compilations, 0);
308 assert_eq!(metrics.size, 0);
309 }
310
311 #[test]
312 fn test_hit_rate_no_compilations() {
313 let cache = CompilationCache::new(CompilationCacheConfig::default());
314 let rate = cache.hit_rate().expect("hit_rate should work");
315 assert_eq!(rate, 0.0);
316 }
317
318 #[test]
319 fn test_clear_cache() {
320 let cache = CompilationCache::new(CompilationCacheConfig::default());
321
322 cache.clear().expect("clear should work");
325
326 let metrics = cache.metrics().expect("metrics should work");
327 assert_eq!(metrics.size, 0);
328 }
329
330 #[test]
331 fn test_cache_config_max_entries_zero_when_disabled() {
332 let config = CompilationCacheConfig {
334 enabled: false,
335 max_entries: 0,
336 };
337 let cache = CompilationCache::new(config);
338 assert!(!cache.config.enabled);
339 }
340
341 #[test]
342 #[should_panic(expected = "max_entries must be > 0 when cache is enabled")]
343 fn test_cache_panics_on_zero_max_entries_when_enabled() {
344 let config = CompilationCacheConfig {
345 enabled: true,
346 max_entries: 0,
347 };
348 let _ = CompilationCache::new(config);
349 }
350
351 #[test]
352 fn test_cache_metrics_clone() {
353 let metrics = CompilationCacheMetrics {
354 hits: 5,
355 misses: 3,
356 total_compilations: 8,
357 size: 2,
358 };
359 let cloned = metrics.clone();
360 assert_eq!(cloned.hits, 5);
361 assert_eq!(cloned.misses, 3);
362 }
363
364 #[test]
365 fn test_cache_config_serialize() {
366 let config = CompilationCacheConfig {
367 enabled: true,
368 max_entries: 50,
369 };
370 let json = serde_json::to_string(&config).expect("serialize should work");
371 let restored: CompilationCacheConfig =
372 serde_json::from_str(&json).expect("deserialize should work");
373 assert_eq!(restored.enabled, config.enabled);
374 assert_eq!(restored.max_entries, config.max_entries);
375 }
376
377 #[test]
378 fn test_compilation_cache_metrics_serialize() {
379 let metrics = CompilationCacheMetrics {
380 hits: 10,
381 misses: 5,
382 total_compilations: 15,
383 size: 3,
384 };
385 let json = serde_json::to_string(&metrics).expect("serialize should work");
386 let restored: CompilationCacheMetrics =
387 serde_json::from_str(&json).expect("deserialize should work");
388 assert_eq!(restored.hits, 10);
389 assert_eq!(restored.size, 3);
390 }
391}