subx_cli/services/ai/cache.rs
1//! AI analysis result caching system for performance optimization.
2//!
3//! This module provides a high-performance caching layer for AI analysis results,
4//! reducing the cost and latency of repeated content analysis operations. The cache
5//! uses intelligent key generation and TTL-based expiration to balance performance
6//! with data freshness.
7//!
8//! # Cache Architecture
9//!
10//! ## Key Generation
11//! - **Content-Based Keys**: Hash-based keys derived from request content
12//! - **Deterministic Hashing**: Consistent keys for identical requests
13//! - **Collision Resistance**: Low probability of hash collisions
14//! - **Efficient Lookup**: O(1) average case lookup performance
15//!
16//! ## Storage Strategy
17//! - **In-Memory Storage**: Fast access using HashMap data structure
18//! - **TTL Expiration**: Time-based cache invalidation for freshness
19//! - **LRU Eviction**: Least Recently Used eviction for memory management
20//! - **Concurrent Access**: Thread-safe operations using RwLock
21//!
22//! ## Performance Benefits
23//! - **Cost Reduction**: Avoid expensive AI API calls for repeated requests
24//! - **Latency Improvement**: Sub-millisecond response time for cached results
25//! - **Rate Limit Compliance**: Reduce API usage to stay within provider limits
26//! - **Offline Operation**: Serve cached results when API is unavailable
27//!
28//! # Usage Examples
29//!
30//! ## Basic Caching Operation
31//! ```rust,ignore
32//! use subx_cli::services::ai::{AICache, AnalysisRequest};
33//! use std::time::Duration;
34//!
35//! // Create cache with 1-hour TTL
36//! let cache = AICache::new(Duration::from_secs(3600));
37//!
38//! // Check for cached result
39//! let request = AnalysisRequest { /* ... */ };
40//! if let Some(cached_result) = cache.get(&request).await {
41//! println!("Using cached result: {:?}", cached_result);
42//! return Ok(cached_result);
43//! }
44//!
45//! // Perform AI analysis and cache result
46//! let fresh_result = ai_client.analyze_content(request.clone()).await?;
47//! cache.put(request, fresh_result.clone()).await;
48//! ```
49//!
50//! ## Cache Management
51//! ```rust,ignore
52//! use subx_cli::services::ai::AICache;
53//!
54//! let cache = AICache::new(Duration::from_secs(1800)); // 30 minutes
55//!
56//! // Get cache statistics
57//! let stats = cache.stats().await;
58//! println!("Cache hits: {}, misses: {}, size: {}",
59//! stats.hits, stats.misses, stats.size);
60//!
61//! // Clear expired entries
62//! let expired_count = cache.cleanup_expired().await;
63//! println!("Removed {} expired entries", expired_count);
64//!
65//! // Clear all cache entries
66//! cache.clear().await;
67//! ```
68
69use crate::services::ai::{AnalysisRequest, MatchResult};
70use std::collections::HashMap;
71use std::collections::hash_map::DefaultHasher;
72use std::hash::{Hash, Hasher};
73use std::time::{Duration, SystemTime};
74use tokio::sync::RwLock;
75
76/// AI analysis result cache
77pub struct AICache {
78 cache: RwLock<HashMap<String, CacheEntry>>,
79 ttl: Duration,
80}
81
82#[cfg(test)]
83mod tests {
84 use super::{AICache, AnalysisRequest, MatchResult};
85 use crate::services::ai::ContentSample;
86 use std::time::Duration;
87 use tokio::time::sleep;
88
89 fn make_request() -> AnalysisRequest {
90 AnalysisRequest {
91 video_files: vec!["video1.mp4".to_string()],
92 subtitle_files: vec!["sub1.srt".to_string()],
93 content_samples: vec![ContentSample {
94 subtitle_file_id: "sub_id".to_string(),
95 filename: "sub1.srt".to_string(),
96 content_preview: "test".to_string(),
97 file_size: 123,
98 }],
99 }
100 }
101
102 #[tokio::test]
103 async fn test_cache_get_set_and_generate_key() {
104 let cache = AICache::new(Duration::from_secs(60));
105 let key = AICache::generate_key(&make_request());
106 // cache miss
107 assert!(cache.get(&key).await.is_none());
108
109 let result = MatchResult {
110 matches: vec![],
111 confidence: 0.5,
112 reasoning: "ok".to_string(),
113 };
114 cache.set(key.clone(), result.clone()).await;
115 // cache hit
116 assert_eq!(cache.get(&key).await, Some(result));
117 }
118
119 #[tokio::test]
120 async fn test_cache_expiration() {
121 let cache = AICache::new(Duration::from_millis(50));
122 let key = "expire".to_string();
123 let result = MatchResult {
124 matches: vec![],
125 confidence: 1.0,
126 reasoning: "expire".to_string(),
127 };
128 cache.set(key.clone(), result).await;
129 // immediate hit
130 assert!(cache.get(&key).await.is_some());
131 sleep(Duration::from_millis(100)).await;
132 // after ttl
133 assert!(cache.get(&key).await.is_none());
134 }
135}
136
137struct CacheEntry {
138 data: MatchResult,
139 created_at: SystemTime,
140}
141
142impl AICache {
143 /// Create a cache with the given TTL (time to live)
144 pub fn new(ttl: Duration) -> Self {
145 Self {
146 cache: RwLock::new(HashMap::new()),
147 ttl,
148 }
149 }
150
151 /// Try to read a result from the cache
152 pub async fn get(&self, key: &str) -> Option<MatchResult> {
153 let cache = self.cache.read().await;
154
155 if let Some(entry) = cache.get(key) {
156 if entry.created_at.elapsed().unwrap_or(Duration::MAX) < self.ttl {
157 return Some(entry.data.clone());
158 }
159 }
160 None
161 }
162
163 /// Write a new result into the cache
164 pub async fn set(&self, key: String, data: MatchResult) {
165 let mut cache = self.cache.write().await;
166 cache.insert(
167 key,
168 CacheEntry {
169 data,
170 created_at: SystemTime::now(),
171 },
172 );
173 }
174
175 /// Generate a cache key based on the request
176 pub fn generate_key(request: &AnalysisRequest) -> String {
177 let mut hasher = DefaultHasher::new();
178 request.video_files.hash(&mut hasher);
179 request.subtitle_files.hash(&mut hasher);
180 format!("{:x}", hasher.finish())
181 }
182}