1use bytes::Bytes;
9use lru::LruCache;
10use std::fmt;
11use std::hash::Hash;
12use std::num::NonZeroUsize;
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16use thiserror::Error;
17use tracing::{debug, trace, warn};
18
19#[derive(Debug, Error)]
21pub enum CacheError {
22 #[error("Cache I/O error: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("Invalid cache key")]
28 InvalidKey,
29
30 #[error("Cache is full")]
32 Full,
33
34 #[error("Cache lock poisoned (a panic occurred while a lock was held)")]
36 Poisoned,
37}
38
39pub type CacheResult<T> = Result<T, CacheError>;
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct CacheKey {
45 pub layer: String,
47
48 pub z: u8,
50
51 pub x: u32,
53
54 pub y: u32,
56
57 pub format: String,
59
60 pub style: Option<String>,
62}
63
64impl CacheKey {
65 pub fn new(layer: String, z: u8, x: u32, y: u32, format: String) -> Self {
67 Self {
68 layer,
69 z,
70 x,
71 y,
72 format,
73 style: None,
74 }
75 }
76
77 pub fn with_style(mut self, style: String) -> Self {
79 self.style = Some(style);
80 self
81 }
82
83 pub fn to_path(&self, base_dir: &Path) -> PathBuf {
85 let mut path = base_dir.to_path_buf();
86 path.push(&self.layer);
87
88 if let Some(ref style) = self.style {
89 path.push(style);
90 }
91
92 path.push(self.z.to_string());
93 path.push(self.x.to_string());
94 path.push(format!("{}.{}", self.y, self.format));
95 path
96 }
97}
98
99impl fmt::Display for CacheKey {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 if let Some(ref style) = self.style {
102 write!(
103 f,
104 "{}/{}/{}/{}/{}.{}",
105 self.layer, style, self.z, self.x, self.y, self.format
106 )
107 } else {
108 write!(
109 f,
110 "{}/{}/{}/{}.{}",
111 self.layer, self.z, self.x, self.y, self.format
112 )
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
119struct CacheEntry {
120 data: Bytes,
122
123 created_at: Instant,
125
126 size: usize,
128
129 access_count: u64,
131}
132
133impl CacheEntry {
134 fn new(data: Bytes) -> Self {
136 let size = data.len();
137 Self {
138 data,
139 created_at: Instant::now(),
140 size,
141 access_count: 0,
142 }
143 }
144
145 fn is_expired(&self, ttl: Duration) -> bool {
147 self.created_at.elapsed() > ttl
148 }
149
150 fn record_access(&mut self) {
152 self.access_count += 1;
153 }
154}
155
156#[derive(Debug, Clone, Default)]
158pub struct CacheStats {
159 pub hits: u64,
161
162 pub misses: u64,
164
165 pub entry_count: usize,
167
168 pub total_size: usize,
170
171 pub evictions: u64,
173
174 pub expirations: u64,
176
177 pub disk_reads: u64,
179
180 pub disk_writes: u64,
182
183 pub put_failures: u64,
185}
186
187impl CacheStats {
188 pub fn hit_rate(&self) -> f64 {
190 let total = self.hits + self.misses;
191 if total == 0 {
192 0.0
193 } else {
194 self.hits as f64 / total as f64
195 }
196 }
197
198 pub fn avg_entry_size(&self) -> f64 {
200 if self.entry_count == 0 {
201 0.0
202 } else {
203 self.total_size as f64 / self.entry_count as f64
204 }
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct TileCacheConfig {
211 pub max_memory_bytes: usize,
213
214 pub disk_cache_dir: Option<PathBuf>,
216
217 pub ttl: Duration,
219
220 pub enable_stats: bool,
222
223 pub compression: bool,
225}
226
227impl Default for TileCacheConfig {
228 fn default() -> Self {
229 Self {
230 max_memory_bytes: 256 * 1024 * 1024, disk_cache_dir: None,
232 ttl: Duration::from_secs(3600), enable_stats: true,
234 compression: false,
235 }
236 }
237}
238
239pub struct TileCache {
241 memory_cache: Arc<Mutex<LruCache<CacheKey, CacheEntry>>>,
243
244 memory_usage: Arc<Mutex<usize>>,
246
247 config: TileCacheConfig,
249
250 stats: Arc<Mutex<CacheStats>>,
252}
253
254const MIN_CACHE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(100) {
256 Some(n) => n,
257 None => unreachable!(),
258};
259
260impl TileCache {
261 pub fn new(config: TileCacheConfig) -> Self {
263 let estimated_capacity = config.max_memory_bytes / (10 * 1024);
265 let capacity = NonZeroUsize::new(estimated_capacity)
267 .unwrap_or(MIN_CACHE_CAPACITY)
268 .max(MIN_CACHE_CAPACITY);
269
270 Self {
271 memory_cache: Arc::new(Mutex::new(LruCache::new(capacity))),
272 memory_usage: Arc::new(Mutex::new(0)),
273 config,
274 stats: Arc::new(Mutex::new(CacheStats::default())),
275 }
276 }
277
278 pub fn get(&self, key: &CacheKey) -> Option<Bytes> {
280 trace!("Cache lookup: {}", key.to_string());
281
282 if let Some(data) = self.get_from_memory(key) {
284 self.record_hit();
285 return Some(data);
286 }
287
288 if self.config.disk_cache_dir.is_some()
290 && let Some(data) = self.get_from_disk(key)
291 {
292 if let Err(e) = self.put_in_memory(key.clone(), data.clone()) {
296 warn!("Failed to promote disk-cache hit into memory cache: {}", e);
297 self.record_put_failure();
298 }
299 self.record_hit();
300 return Some(data);
301 }
302
303 self.record_miss();
304 None
305 }
306
307 pub fn put(&self, key: CacheKey, data: Bytes) -> CacheResult<()> {
309 trace!("Caching tile: {}", key.to_string());
310
311 self.put_in_memory(key.clone(), data.clone())?;
313
314 if self.config.disk_cache_dir.is_some()
319 && let Err(e) = self.put_on_disk(&key, &data)
320 {
321 warn!("Failed to write tile to disk cache ({}): {}", key, e);
322 self.record_put_failure();
323 }
324
325 Ok(())
326 }
327
328 fn get_from_memory(&self, key: &CacheKey) -> Option<Bytes> {
330 let mut cache = self.memory_cache.lock().ok()?;
331
332 let is_expired = if let Some(entry) = cache.peek(key) {
334 entry.is_expired(self.config.ttl)
335 } else {
336 return None;
337 };
338
339 if is_expired {
340 trace!("Entry expired: {}", key.to_string());
341 self.record_expiration();
342 let entry = cache.pop(key)?;
343 self.update_memory_usage(|usage| usage.saturating_sub(entry.size));
344 return None;
345 }
346
347 if let Some(entry) = cache.get_mut(key) {
349 entry.record_access();
350 Some(entry.data.clone())
351 } else {
352 None
353 }
354 }
355
356 fn put_in_memory(&self, key: CacheKey, data: Bytes) -> CacheResult<()> {
358 let entry = CacheEntry::new(data);
359 let entry_size = entry.size;
360
361 let mut cache = self.memory_cache.lock().map_err(|_| CacheError::Full)?;
362
363 while self.get_memory_usage() + entry_size > self.config.max_memory_bytes {
365 if let Some((_, evicted)) = cache.pop_lru() {
366 debug!("Evicting entry from memory cache");
367 self.update_memory_usage(|usage| usage.saturating_sub(evicted.size));
368 self.record_eviction();
369 } else {
370 break;
371 }
372 }
373
374 if let Some(old_entry) = cache.put(key, entry) {
376 self.update_memory_usage(|usage| usage.saturating_sub(old_entry.size));
377 }
378
379 self.update_memory_usage(|usage| usage + entry_size);
380
381 Ok(())
382 }
383
384 fn get_from_disk(&self, key: &CacheKey) -> Option<Bytes> {
386 let base_dir = self.config.disk_cache_dir.as_ref()?;
387 let path = key.to_path(base_dir);
388
389 match std::fs::read(&path) {
390 Ok(data) => {
391 trace!("Disk cache hit: {}", path.display());
392 self.record_disk_read();
393 Some(Bytes::from(data))
394 }
395 Err(_) => None,
396 }
397 }
398
399 fn put_on_disk(&self, key: &CacheKey, data: &Bytes) -> CacheResult<()> {
401 let base_dir =
402 self.config
403 .disk_cache_dir
404 .as_ref()
405 .ok_or(CacheError::Io(std::io::Error::new(
406 std::io::ErrorKind::NotFound,
407 "No disk cache directory",
408 )))?;
409
410 let path = key.to_path(base_dir);
411
412 if let Some(parent) = path.parent() {
414 std::fs::create_dir_all(parent)?;
415 }
416
417 std::fs::write(&path, data)?;
419 self.record_disk_write();
420
421 trace!("Wrote to disk cache: {}", path.display());
422 Ok(())
423 }
424
425 pub fn clear(&self) -> CacheResult<()> {
432 match self.memory_cache.lock() {
434 Ok(mut cache) => cache.clear(),
435 Err(poison) => {
436 warn!("memory_cache lock poisoned during clear(); recovering and clearing");
437 poison.into_inner().clear();
438 }
439 }
440
441 self.update_memory_usage(|_| 0);
442
443 if let Some(ref dir) = self.config.disk_cache_dir
445 && dir.exists()
446 {
447 std::fs::remove_dir_all(dir)?;
448 std::fs::create_dir_all(dir)?;
449 }
450
451 match self.stats.lock() {
453 Ok(mut stats) => *stats = CacheStats::default(),
454 Err(poison) => {
455 warn!("stats lock poisoned during clear(); recovering and resetting");
456 *poison.into_inner() = CacheStats::default();
457 }
458 }
459
460 debug!("Cache cleared");
461 Ok(())
462 }
463
464 pub fn stats(&self) -> CacheStats {
470 match self.stats.lock() {
471 Ok(stats) => stats.clone(),
472 Err(poison) => {
473 warn!("stats lock poisoned; returning last-known stats");
474 poison.into_inner().clone()
475 }
476 }
477 }
478
479 fn get_memory_usage(&self) -> usize {
481 match self.memory_usage.lock() {
482 Ok(usage) => *usage,
483 Err(poison) => {
484 warn!("memory_usage lock poisoned; returning last-known usage");
485 *poison.into_inner()
486 }
487 }
488 }
489
490 fn update_memory_usage<F>(&self, f: F)
492 where
493 F: FnOnce(usize) -> usize,
494 {
495 if let Ok(mut usage) = self.memory_usage.lock() {
496 *usage = f(*usage);
497 }
498
499 if let Ok(mut stats) = self.stats.lock() {
500 stats.total_size = self.get_memory_usage();
501 }
502 }
503
504 fn record_hit(&self) {
506 if self.config.enable_stats
507 && let Ok(mut stats) = self.stats.lock()
508 {
509 stats.hits += 1;
510 }
511 }
512
513 fn record_miss(&self) {
515 if self.config.enable_stats
516 && let Ok(mut stats) = self.stats.lock()
517 {
518 stats.misses += 1;
519 }
520 }
521
522 fn record_eviction(&self) {
524 if self.config.enable_stats
525 && let Ok(mut stats) = self.stats.lock()
526 {
527 stats.evictions += 1;
528 }
529 }
530
531 fn record_expiration(&self) {
533 if self.config.enable_stats
534 && let Ok(mut stats) = self.stats.lock()
535 {
536 stats.expirations += 1;
537 }
538 }
539
540 fn record_disk_read(&self) {
542 if self.config.enable_stats
543 && let Ok(mut stats) = self.stats.lock()
544 {
545 stats.disk_reads += 1;
546 }
547 }
548
549 fn record_disk_write(&self) {
551 if self.config.enable_stats
552 && let Ok(mut stats) = self.stats.lock()
553 {
554 stats.disk_writes += 1;
555 }
556 }
557
558 fn record_put_failure(&self) {
560 if self.config.enable_stats
561 && let Ok(mut stats) = self.stats.lock()
562 {
563 stats.put_failures += 1;
564 }
565 }
566}
567
568impl Clone for TileCache {
569 fn clone(&self) -> Self {
570 Self {
571 memory_cache: Arc::clone(&self.memory_cache),
572 memory_usage: Arc::clone(&self.memory_usage),
573 config: self.config.clone(),
574 stats: Arc::clone(&self.stats),
575 }
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn test_cache_key_to_string() {
585 let key = CacheKey::new("landsat".to_string(), 10, 512, 384, "png".to_string());
586 assert_eq!(key.to_string(), "landsat/10/512/384.png");
587
588 let key_with_style = key.with_style("default".to_string());
589 assert_eq!(key_with_style.to_string(), "landsat/default/10/512/384.png");
590 }
591
592 #[test]
593 fn test_cache_put_get() {
594 let config = TileCacheConfig::default();
595 let cache = TileCache::new(config);
596
597 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
598 let data = Bytes::from(vec![1, 2, 3, 4, 5]);
599
600 cache.put(key.clone(), data.clone()).expect("put failed");
601
602 let retrieved = cache.get(&key).expect("get failed");
603 assert_eq!(retrieved, data);
604 }
605
606 #[test]
607 fn test_cache_miss() {
608 let config = TileCacheConfig::default();
609 let cache = TileCache::new(config);
610
611 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
612 assert!(cache.get(&key).is_none());
613
614 let stats = cache.stats();
615 assert_eq!(stats.misses, 1);
616 assert_eq!(stats.hits, 0);
617 }
618
619 #[test]
620 fn test_cache_stats() {
621 let config = TileCacheConfig::default();
622 let cache = TileCache::new(config);
623
624 let key1 = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
625 let key2 = CacheKey::new("test".to_string(), 0, 0, 1, "png".to_string());
626 let data = Bytes::from(vec![1, 2, 3]);
627
628 cache.put(key1.clone(), data.clone()).expect("put failed");
629 cache.put(key2.clone(), data.clone()).expect("put failed");
630
631 cache.get(&key1);
632 cache.get(&key2);
633 cache.get(&CacheKey::new(
634 "nonexistent".to_string(),
635 0,
636 0,
637 0,
638 "png".to_string(),
639 ));
640
641 let stats = cache.stats();
642 assert_eq!(stats.hits, 2);
643 assert_eq!(stats.misses, 1);
644 assert!(stats.hit_rate() > 0.6);
645 }
646
647 #[test]
648 fn test_cache_clear() {
649 let config = TileCacheConfig::default();
650 let cache = TileCache::new(config);
651
652 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
653 let data = Bytes::from(vec![1, 2, 3]);
654
655 cache.put(key.clone(), data).expect("put failed");
656 assert!(cache.get(&key).is_some());
657
658 cache.clear().expect("clear failed");
659 assert!(cache.get(&key).is_none());
660 }
661}