1use crate::schema::{RdfVocabulary, SchemaGenerationConfig, SchemaGenerator};
10use crate::types::Schema;
11use anyhow::Result;
12use lru::LruCache;
13use std::collections::HashMap;
14use std::num::NonZeroUsize;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::{Duration, SystemTime};
18use tokio::fs;
19use tokio::sync::RwLock;
20use tokio::time::interval;
21use tracing::{debug, error, info, warn};
22
23#[derive(Debug, Clone)]
25pub struct SchemaCacheEntry {
26 pub schema: Schema,
27 pub created_at: SystemTime,
28 pub last_accessed: SystemTime,
29 pub access_count: u64,
30 pub version: u64,
31 pub source_hash: String,
32}
33
34impl SchemaCacheEntry {
35 pub fn new(schema: Schema, source_hash: String, version: u64) -> Self {
36 let now = SystemTime::now();
37 Self {
38 schema,
39 created_at: now,
40 last_accessed: now,
41 access_count: 0,
42 version,
43 source_hash,
44 }
45 }
46
47 pub fn access(&mut self) {
48 self.access_count += 1;
49 self.last_accessed = SystemTime::now();
50 }
51
52 pub fn age(&self) -> Duration {
53 self.created_at.elapsed().unwrap_or(Duration::from_secs(0))
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct SchemaCacheConfig {
60 pub max_cache_size: usize,
62 pub ttl: Duration,
64 pub enable_hot_reload: bool,
66 pub watch_directory: Option<PathBuf>,
68 pub reload_check_interval: Duration,
70 pub enable_statistics: bool,
72}
73
74impl Default for SchemaCacheConfig {
75 fn default() -> Self {
76 Self {
77 max_cache_size: 100,
78 ttl: Duration::from_secs(3600), enable_hot_reload: true,
80 watch_directory: None,
81 reload_check_interval: Duration::from_secs(5),
82 enable_statistics: true,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct CacheStatistics {
90 pub hits: u64,
91 pub misses: u64,
92 pub evictions: u64,
93 pub reloads: u64,
94 pub total_schemas: u64,
95 pub current_size: usize,
96}
97
98impl CacheStatistics {
99 pub fn hit_rate(&self) -> f64 {
100 let total = self.hits + self.misses;
101 if total == 0 {
102 0.0
103 } else {
104 self.hits as f64 / total as f64
105 }
106 }
107}
108
109pub struct SchemaCache {
111 cache: Arc<RwLock<LruCache<String, SchemaCacheEntry>>>,
112 config: SchemaCacheConfig,
113 statistics: Arc<RwLock<CacheStatistics>>,
114 file_watchers: Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
115 next_version: Arc<RwLock<u64>>,
116}
117
118impl SchemaCache {
119 pub fn new(config: SchemaCacheConfig) -> Self {
121 let cache_size = NonZeroUsize::new(config.max_cache_size)
122 .unwrap_or(NonZeroUsize::new(100).expect("Default cache size should be valid"));
123
124 Self {
125 cache: Arc::new(RwLock::new(LruCache::new(cache_size))),
126 config: config.clone(),
127 statistics: Arc::new(RwLock::new(CacheStatistics::default())),
128 file_watchers: Arc::new(RwLock::new(HashMap::new())),
129 next_version: Arc::new(RwLock::new(1)),
130 }
131 }
132
133 pub async fn get(&self, key: &str) -> Option<Schema> {
135 let mut cache = self.cache.write().await;
136
137 if let Some(entry) = cache.get_mut(key) {
138 if entry.age() > self.config.ttl {
140 debug!("Schema cache entry expired for key: {}", key);
141 cache.pop(key);
142 self.record_miss().await;
143 return None;
144 }
145
146 entry.access();
147 self.record_hit().await;
148 debug!(
149 "Schema cache hit for key: {} (version: {}, age: {:?})",
150 key,
151 entry.version,
152 entry.age()
153 );
154 Some(entry.schema.clone())
155 } else {
156 self.record_miss().await;
157 debug!("Schema cache miss for key: {}", key);
158 None
159 }
160 }
161
162 pub async fn put(&self, key: String, schema: Schema, source_hash: String) {
164 let mut cache = self.cache.write().await;
165 let mut version_guard = self.next_version.write().await;
166 let version = *version_guard;
167 *version_guard += 1;
168
169 let entry = SchemaCacheEntry::new(schema, source_hash, version);
170
171 if cache.len() >= cache.cap().get() && !cache.contains(&key) {
173 self.record_eviction().await;
174 }
175
176 cache.put(key.clone(), entry);
177 info!("Cached schema for key: {} (version: {})", key, version);
178
179 let mut stats = self.statistics.write().await;
181 stats.total_schemas += 1;
182 stats.current_size = cache.len();
183 }
184
185 pub async fn get_or_generate(
187 &self,
188 key: &str,
189 vocabulary: RdfVocabulary,
190 config: SchemaGenerationConfig,
191 ) -> Result<Schema> {
192 if let Some(schema) = self.get(key).await {
194 return Ok(schema);
195 }
196
197 let generator = SchemaGenerator::new()
199 .with_config(config)
200 .with_vocabulary(vocabulary.clone());
201 let schema = generator.generate_schema()?;
202
203 let source_hash = format!("{:?}", vocabulary).len().to_string();
205
206 self.put(key.to_string(), schema.clone(), source_hash).await;
208
209 Ok(schema)
210 }
211
212 pub async fn invalidate(&self, key: &str) -> bool {
214 let mut cache = self.cache.write().await;
215 if cache.pop(key).is_some() {
216 info!("Invalidated schema cache for key: {}", key);
217
218 let mut stats = self.statistics.write().await;
219 stats.current_size = cache.len();
220 true
221 } else {
222 false
223 }
224 }
225
226 pub async fn clear(&self) {
228 let mut cache = self.cache.write().await;
229 let count = cache.len();
230 cache.clear();
231
232 info!("Cleared schema cache ({} entries removed)", count);
233
234 let mut stats = self.statistics.write().await;
235 stats.current_size = 0;
236 }
237
238 pub async fn statistics(&self) -> CacheStatistics {
240 self.statistics.read().await.clone()
241 }
242
243 pub async fn start_hot_reload(&self) {
245 if !self.config.enable_hot_reload {
246 return;
247 }
248
249 let watch_dir = match &self.config.watch_directory {
250 Some(dir) => dir.clone(),
251 None => {
252 warn!("Hot-reload enabled but no watch directory specified");
253 return;
254 }
255 };
256
257 info!(
258 "Starting schema hot-reload watcher for directory: {:?}",
259 watch_dir
260 );
261
262 let cache = Arc::clone(&self.cache);
263 let file_watchers = Arc::clone(&self.file_watchers);
264 let statistics = Arc::clone(&self.statistics);
265 let check_interval = self.config.reload_check_interval;
266
267 tokio::spawn(async move {
268 let mut interval = interval(check_interval);
269
270 loop {
271 interval.tick().await;
272
273 match Self::check_directory_changes(&watch_dir, &file_watchers, &cache, &statistics)
274 .await
275 {
276 Ok(changed) => {
277 if changed > 0 {
278 info!("Hot-reload detected {} schema file changes", changed);
279 }
280 }
281 Err(e) => {
282 error!("Error checking directory changes: {}", e);
283 }
284 }
285 }
286 });
287 }
288
289 async fn check_directory_changes(
291 dir: &Path,
292 watchers: &Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
293 cache: &Arc<RwLock<LruCache<String, SchemaCacheEntry>>>,
294 statistics: &Arc<RwLock<CacheStatistics>>,
295 ) -> Result<usize> {
296 let mut changed_count = 0;
297
298 let mut entries = fs::read_dir(dir).await?;
300
301 let mut current_files = HashMap::new();
302
303 while let Some(entry) = entries.next_entry().await? {
304 let path = entry.path();
305
306 if let Some(ext) = path.extension() {
308 let ext_str = ext.to_string_lossy();
309 if !matches!(ext_str.as_ref(), "ttl" | "rdf" | "owl" | "nt") {
310 continue;
311 }
312 } else {
313 continue;
314 }
315
316 if let Ok(metadata) = fs::metadata(&path).await {
318 if let Ok(modified) = metadata.modified() {
319 current_files.insert(path.clone(), modified);
320 }
321 }
322 }
323
324 let mut watchers_guard = watchers.write().await;
325
326 for (file_path, modified_time) in ¤t_files {
328 match watchers_guard.get(file_path) {
329 Some(cached_time) if cached_time != modified_time => {
330 debug!("Detected change in schema file: {:?}", file_path);
332
333 let mut cache_guard = cache.write().await;
335 cache_guard.clear(); watchers_guard.insert(file_path.clone(), *modified_time);
338 changed_count += 1;
339
340 let mut stats = statistics.write().await;
341 stats.reloads += 1;
342 }
343 None => {
344 debug!("Detected new schema file: {:?}", file_path);
346 watchers_guard.insert(file_path.clone(), *modified_time);
347 changed_count += 1;
348 }
349 _ => {}
350 }
351 }
352
353 let deleted: Vec<_> = watchers_guard
355 .keys()
356 .filter(|path| !current_files.contains_key(*path))
357 .cloned()
358 .collect();
359
360 for path in deleted {
361 debug!("Detected deletion of schema file: {:?}", path);
362 watchers_guard.remove(&path);
363 changed_count += 1;
364 }
365
366 Ok(changed_count)
367 }
368
369 async fn record_hit(&self) {
371 if self.config.enable_statistics {
372 let mut stats = self.statistics.write().await;
373 stats.hits += 1;
374 }
375 }
376
377 async fn record_miss(&self) {
379 if self.config.enable_statistics {
380 let mut stats = self.statistics.write().await;
381 stats.misses += 1;
382 }
383 }
384
385 async fn record_eviction(&self) {
387 if self.config.enable_statistics {
388 let mut stats = self.statistics.write().await;
389 stats.evictions += 1;
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use std::collections::HashMap;
398
399 fn create_test_schema() -> Schema {
400 Schema::new()
401 }
402
403 #[allow(dead_code)]
404 fn create_test_vocabulary() -> RdfVocabulary {
405 RdfVocabulary {
406 classes: HashMap::new(),
407 properties: HashMap::new(),
408 namespaces: HashMap::new(),
409 }
410 }
411
412 #[tokio::test]
413 async fn test_schema_cache_creation() {
414 let config = SchemaCacheConfig::default();
415 let cache = SchemaCache::new(config);
416
417 let stats = cache.statistics().await;
418 assert_eq!(stats.hits, 0);
419 assert_eq!(stats.misses, 0);
420 assert_eq!(stats.current_size, 0);
421 }
422
423 #[tokio::test]
424 async fn test_schema_cache_put_and_get() {
425 let config = SchemaCacheConfig::default();
426 let cache = SchemaCache::new(config);
427
428 let schema = create_test_schema();
429 let key = "test_schema".to_string();
430 let hash = "test_hash".to_string();
431
432 cache.put(key.clone(), schema.clone(), hash).await;
433
434 let retrieved = cache.get(&key).await;
435 assert!(retrieved.is_some());
436
437 let stats = cache.statistics().await;
438 assert_eq!(stats.hits, 1);
439 assert_eq!(stats.misses, 0);
440 }
441
442 #[tokio::test]
443 async fn test_schema_cache_miss() {
444 let config = SchemaCacheConfig::default();
445 let cache = SchemaCache::new(config);
446
447 let retrieved = cache.get("nonexistent").await;
448 assert!(retrieved.is_none());
449
450 let stats = cache.statistics().await;
451 assert_eq!(stats.hits, 0);
452 assert_eq!(stats.misses, 1);
453 }
454
455 #[tokio::test]
456 async fn test_schema_cache_invalidation() {
457 let config = SchemaCacheConfig::default();
458 let cache = SchemaCache::new(config);
459
460 let schema = create_test_schema();
461 let key = "test_schema".to_string();
462 let hash = "test_hash".to_string();
463
464 cache.put(key.clone(), schema, hash).await;
465 assert!(cache.get(&key).await.is_some());
466
467 cache.invalidate(&key).await;
468 assert!(cache.get(&key).await.is_none());
469 }
470
471 #[tokio::test]
472 async fn test_schema_cache_clear() {
473 let config = SchemaCacheConfig::default();
474 let cache = SchemaCache::new(config);
475
476 for i in 0..5 {
477 let schema = create_test_schema();
478 let key = format!("schema_{}", i);
479 let hash = format!("hash_{}", i);
480 cache.put(key, schema, hash).await;
481 }
482
483 let stats = cache.statistics().await;
484 assert_eq!(stats.current_size, 5);
485
486 cache.clear().await;
487
488 let stats = cache.statistics().await;
489 assert_eq!(stats.current_size, 0);
490 }
491
492 #[tokio::test]
493 async fn test_schema_cache_statistics() {
494 let config = SchemaCacheConfig::default();
495 let cache = SchemaCache::new(config);
496
497 let schema = create_test_schema();
498 cache
499 .put("schema1".to_string(), schema.clone(), "hash1".to_string())
500 .await;
501
502 let _ = cache.get("schema1").await;
504
505 let _ = cache.get("schema2").await;
507
508 let stats = cache.statistics().await;
509 assert_eq!(stats.hits, 1);
510 assert_eq!(stats.misses, 1);
511 assert_eq!(stats.hit_rate(), 0.5);
512 }
513
514 #[tokio::test]
515 async fn test_schema_cache_ttl_expiration() {
516 let config = SchemaCacheConfig {
517 ttl: Duration::from_millis(100), ..Default::default()
519 };
520
521 let cache = SchemaCache::new(config);
522 let schema = create_test_schema();
523
524 cache
525 .put("test".to_string(), schema, "hash".to_string())
526 .await;
527
528 assert!(cache.get("test").await.is_some());
530
531 tokio::time::sleep(Duration::from_millis(150)).await;
533
534 assert!(cache.get("test").await.is_none());
536 }
537
538 #[tokio::test]
539 async fn test_cache_entry_versioning() {
540 let schema = create_test_schema();
541 let entry1 = SchemaCacheEntry::new(schema.clone(), "hash1".to_string(), 1);
542 let entry2 = SchemaCacheEntry::new(schema, "hash2".to_string(), 2);
543
544 assert_eq!(entry1.version, 1);
545 assert_eq!(entry2.version, 2);
546 assert_ne!(entry1.source_hash, entry2.source_hash);
547 }
548
549 #[tokio::test]
550 async fn test_cache_entry_access_tracking() {
551 let schema = create_test_schema();
552 let mut entry = SchemaCacheEntry::new(schema, "hash".to_string(), 1);
553
554 assert_eq!(entry.access_count, 0);
555
556 entry.access();
557 assert_eq!(entry.access_count, 1);
558
559 entry.access();
560 assert_eq!(entry.access_count, 2);
561 }
562}