1use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::hash::Hash;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CacheConfig {
21 pub enabled: bool,
23 pub default_ttl_seconds: u64,
25 pub max_size_mb: u64,
27 pub backend: CacheBackend,
29 pub cdn: Option<CdnConfig>,
31 pub rules: Vec<CacheRule>,
33}
34
35impl Default for CacheConfig {
36 fn default() -> Self {
37 Self {
38 enabled: true,
39 default_ttl_seconds: 300,
40 max_size_mb: 100,
41 backend: CacheBackend::Memory,
42 cdn: None,
43 rules: vec![
44 CacheRule {
45 pattern: "/api/stats".to_string(),
46 ttl_seconds: 60,
47 cache_control: "public, max-age=60".to_string(),
48 vary: vec!["Accept".to_string()],
49 private: false,
50 },
51 CacheRule {
52 pattern: "/api/sessions".to_string(),
53 ttl_seconds: 30,
54 cache_control: "private, max-age=30".to_string(),
55 vary: vec!["Authorization".to_string()],
56 private: true,
57 },
58 ],
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(rename_all = "snake_case")]
66pub enum CacheBackend {
67 Memory,
69 Redis(String),
71 Memcached(String),
73 File(String),
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct CdnConfig {
80 pub provider: CdnProvider,
82 pub base_url: String,
84 pub api_key: Option<String>,
86 pub zone_id: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92#[serde(rename_all = "snake_case")]
93pub enum CdnProvider {
94 Cloudflare,
95 Fastly,
96 CloudFront,
97 Akamai,
98 BunnyCDN,
99 Custom,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CacheRule {
105 pub pattern: String,
107 pub ttl_seconds: u64,
109 pub cache_control: String,
111 pub vary: Vec<String>,
113 pub private: bool,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct CacheEntry {
124 pub key: String,
126 pub value: Vec<u8>,
128 pub content_type: String,
130 pub etag: String,
132 pub created_at: DateTime<Utc>,
134 pub expires_at: DateTime<Utc>,
136 pub headers: HashMap<String, String>,
138 pub hits: u64,
140 pub size_bytes: usize,
142}
143
144impl CacheEntry {
145 pub fn is_expired(&self) -> bool {
147 Utc::now() > self.expires_at
148 }
149
150 pub fn is_stale(&self, grace_seconds: i64) -> bool {
152 let grace_time = self.expires_at + Duration::seconds(grace_seconds);
153 Utc::now() > self.expires_at && Utc::now() <= grace_time
154 }
155
156 pub fn remaining_ttl(&self) -> i64 {
158 (self.expires_at - Utc::now()).num_seconds().max(0)
159 }
160}
161
162pub struct EdgeCacheManager {
168 config: CacheConfig,
169 cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
170 stats: Arc<RwLock<CacheStats>>,
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct CacheStats {
176 pub requests: u64,
178 pub hits: u64,
180 pub misses: u64,
182 pub stale_hits: u64,
184 pub bytes_served: u64,
186 pub current_size_bytes: u64,
188 pub entry_count: usize,
190 pub evictions: u64,
192}
193
194impl CacheStats {
195 pub fn hit_rate(&self) -> f64 {
197 if self.requests == 0 {
198 0.0
199 } else {
200 self.hits as f64 / self.requests as f64
201 }
202 }
203}
204
205impl EdgeCacheManager {
206 pub fn new(config: CacheConfig) -> Self {
208 Self {
209 config,
210 cache: Arc::new(RwLock::new(HashMap::new())),
211 stats: Arc::new(RwLock::new(CacheStats::default())),
212 }
213 }
214
215 pub fn generate_key(&self, path: &str, query: Option<&str>, vary_headers: &HashMap<String, String>) -> String {
217 let mut key = path.to_string();
218
219 if let Some(q) = query {
220 key.push('?');
221 key.push_str(q);
222 }
223
224 let rule = self.get_rule(path);
226 if let Some(rule) = rule {
227 for header in &rule.vary {
228 if let Some(value) = vary_headers.get(header) {
229 key.push_str(&format!("|{}:{}", header, value));
230 }
231 }
232 }
233
234 format!("cache:{:x}", md5_hash(&key))
236 }
237
238 fn get_rule(&self, path: &str) -> Option<&CacheRule> {
240 self.config.rules.iter().find(|r| path.starts_with(&r.pattern))
241 }
242
243 pub async fn get(&self, key: &str) -> Option<CacheEntry> {
245 let mut stats = self.stats.write().await;
246 stats.requests += 1;
247
248 let cache = self.cache.read().await;
249 if let Some(entry) = cache.get(key) {
250 if !entry.is_expired() {
251 stats.hits += 1;
252 stats.bytes_served += entry.size_bytes as u64;
253 return Some(entry.clone());
254 } else if entry.is_stale(60) {
255 stats.stale_hits += 1;
257 stats.bytes_served += entry.size_bytes as u64;
258 return Some(entry.clone());
259 }
260 }
261
262 stats.misses += 1;
263 None
264 }
265
266 pub async fn set(&self, key: String, value: Vec<u8>, content_type: String, path: &str) {
268 let rule = self.get_rule(path);
269 let ttl = rule.map(|r| r.ttl_seconds).unwrap_or(self.config.default_ttl_seconds);
270 let cache_control = rule
271 .map(|r| r.cache_control.clone())
272 .unwrap_or_else(|| format!("public, max-age={}", ttl));
273
274 let entry = CacheEntry {
275 key: key.clone(),
276 size_bytes: value.len(),
277 value,
278 content_type,
279 etag: generate_etag(&key),
280 created_at: Utc::now(),
281 expires_at: Utc::now() + Duration::seconds(ttl as i64),
282 headers: HashMap::from([("Cache-Control".to_string(), cache_control)]),
283 hits: 0,
284 };
285
286 self.evict_if_needed(entry.size_bytes).await;
288
289 let mut cache = self.cache.write().await;
290 let mut stats = self.stats.write().await;
291
292 stats.current_size_bytes += entry.size_bytes as u64;
293 stats.entry_count = cache.len() + 1;
294
295 cache.insert(key, entry);
296 }
297
298 async fn evict_if_needed(&self, new_entry_size: usize) {
300 let max_size = self.config.max_size_mb * 1024 * 1024;
301 let stats = self.stats.read().await;
302
303 if stats.current_size_bytes + new_entry_size as u64 <= max_size {
304 return;
305 }
306 drop(stats);
307
308 self.evict_expired().await;
310
311 let stats = self.stats.read().await;
313 if stats.current_size_bytes + new_entry_size as u64 > max_size {
314 drop(stats);
315 self.evict_lru((max_size / 4) as usize).await; }
317 }
318
319 async fn evict_expired(&self) {
321 let mut cache = self.cache.write().await;
322 let mut stats = self.stats.write().await;
323
324 let expired_keys: Vec<_> = cache
325 .iter()
326 .filter(|(_, entry)| entry.is_expired() && !entry.is_stale(60))
327 .map(|(k, _)| k.clone())
328 .collect();
329
330 for key in expired_keys {
331 if let Some(entry) = cache.remove(&key) {
332 stats.current_size_bytes -= entry.size_bytes as u64;
333 stats.evictions += 1;
334 }
335 }
336 stats.entry_count = cache.len();
337 }
338
339 async fn evict_lru(&self, bytes_to_free: usize) {
341 let mut cache = self.cache.write().await;
342 let mut stats = self.stats.write().await;
343
344 let mut entries: Vec<_> = cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
346 entries.sort_by(|a, b| a.1.hits.cmp(&b.1.hits).then(a.1.created_at.cmp(&b.1.created_at)));
347
348 let mut freed = 0usize;
349 for (key, entry) in entries {
350 if freed >= bytes_to_free {
351 break;
352 }
353 cache.remove(&key);
354 freed += entry.size_bytes;
355 stats.current_size_bytes -= entry.size_bytes as u64;
356 stats.evictions += 1;
357 }
358 stats.entry_count = cache.len();
359 }
360
361 pub async fn invalidate(&self, key: &str) {
363 let mut cache = self.cache.write().await;
364 let mut stats = self.stats.write().await;
365
366 if let Some(entry) = cache.remove(key) {
367 stats.current_size_bytes -= entry.size_bytes as u64;
368 stats.entry_count = cache.len();
369 }
370
371 if let Some(cdn) = &self.config.cdn {
373 self.invalidate_cdn(cdn, key).await;
374 }
375 }
376
377 pub async fn invalidate_prefix(&self, prefix: &str) {
379 let mut cache = self.cache.write().await;
380 let mut stats = self.stats.write().await;
381
382 let keys_to_remove: Vec<_> = cache
383 .keys()
384 .filter(|k| k.starts_with(prefix))
385 .cloned()
386 .collect();
387
388 for key in keys_to_remove {
389 if let Some(entry) = cache.remove(&key) {
390 stats.current_size_bytes -= entry.size_bytes as u64;
391 }
392 }
393 stats.entry_count = cache.len();
394 }
395
396 pub async fn clear(&self) {
398 let mut cache = self.cache.write().await;
399 let mut stats = self.stats.write().await;
400
401 cache.clear();
402 stats.current_size_bytes = 0;
403 stats.entry_count = 0;
404 }
405
406 async fn invalidate_cdn(&self, cdn: &CdnConfig, key: &str) {
408 match cdn.provider {
409 CdnProvider::Cloudflare => self.invalidate_cloudflare(cdn, key).await,
410 CdnProvider::Fastly => self.invalidate_fastly(cdn, key).await,
411 CdnProvider::CloudFront => self.invalidate_cloudfront(cdn, key).await,
412 _ => {}
413 }
414 }
415
416 async fn invalidate_cloudflare(&self, _cdn: &CdnConfig, _key: &str) {
417 }
420
421 async fn invalidate_fastly(&self, _cdn: &CdnConfig, _key: &str) {
422 }
425
426 async fn invalidate_cloudfront(&self, _cdn: &CdnConfig, _key: &str) {
427 }
430
431 pub async fn get_stats(&self) -> CacheStats {
433 self.stats.read().await.clone()
434 }
435
436 pub fn get_cache_headers(&self, path: &str, etag: &str) -> HashMap<String, String> {
438 let mut headers = HashMap::new();
439
440 if let Some(rule) = self.get_rule(path) {
441 headers.insert("Cache-Control".to_string(), rule.cache_control.clone());
442 if !rule.vary.is_empty() {
443 headers.insert("Vary".to_string(), rule.vary.join(", "));
444 }
445 } else {
446 headers.insert(
447 "Cache-Control".to_string(),
448 format!("public, max-age={}", self.config.default_ttl_seconds),
449 );
450 }
451
452 headers.insert("ETag".to_string(), format!("\"{}\"", etag));
453 headers
454 }
455}
456
457fn md5_hash(input: &str) -> u128 {
462 use std::hash::Hasher;
463 let mut hasher = std::collections::hash_map::DefaultHasher::new();
464 input.hash(&mut hasher);
465 hasher.finish() as u128
466}
467
468fn generate_etag(key: &str) -> String {
469 format!("{:x}", md5_hash(&format!("{}{}", key, Utc::now().timestamp())))
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[tokio::test]
477 async fn test_cache_set_get() {
478 let config = CacheConfig::default();
479 let cache = EdgeCacheManager::new(config);
480
481 let key = cache.generate_key("/api/stats", None, &HashMap::new());
482 cache.set(key.clone(), b"test data".to_vec(), "application/json".to_string(), "/api/stats").await;
483
484 let entry = cache.get(&key).await;
485 assert!(entry.is_some());
486 assert_eq!(entry.unwrap().value, b"test data");
487 }
488
489 #[tokio::test]
490 async fn test_cache_invalidation() {
491 let config = CacheConfig::default();
492 let cache = EdgeCacheManager::new(config);
493
494 let key = cache.generate_key("/api/test", None, &HashMap::new());
495 cache.set(key.clone(), b"test".to_vec(), "text/plain".to_string(), "/api/test").await;
496
497 assert!(cache.get(&key).await.is_some());
498
499 cache.invalidate(&key).await;
500 let stats = cache.get_stats().await;
502 assert_eq!(stats.entry_count, 0);
503 }
504}