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};
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
35pub type CacheResult<T> = Result<T, CacheError>;
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct CacheKey {
41 pub layer: String,
43
44 pub z: u8,
46
47 pub x: u32,
49
50 pub y: u32,
52
53 pub format: String,
55
56 pub style: Option<String>,
58}
59
60impl CacheKey {
61 pub fn new(layer: String, z: u8, x: u32, y: u32, format: String) -> Self {
63 Self {
64 layer,
65 z,
66 x,
67 y,
68 format,
69 style: None,
70 }
71 }
72
73 pub fn with_style(mut self, style: String) -> Self {
75 self.style = Some(style);
76 self
77 }
78
79 pub fn to_path(&self, base_dir: &Path) -> PathBuf {
81 let mut path = base_dir.to_path_buf();
82 path.push(&self.layer);
83
84 if let Some(ref style) = self.style {
85 path.push(style);
86 }
87
88 path.push(self.z.to_string());
89 path.push(self.x.to_string());
90 path.push(format!("{}.{}", self.y, self.format));
91 path
92 }
93}
94
95impl fmt::Display for CacheKey {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 if let Some(ref style) = self.style {
98 write!(
99 f,
100 "{}/{}/{}/{}/{}.{}",
101 self.layer, style, self.z, self.x, self.y, self.format
102 )
103 } else {
104 write!(
105 f,
106 "{}/{}/{}/{}.{}",
107 self.layer, self.z, self.x, self.y, self.format
108 )
109 }
110 }
111}
112
113#[derive(Debug, Clone)]
115struct CacheEntry {
116 data: Bytes,
118
119 created_at: Instant,
121
122 size: usize,
124
125 access_count: u64,
127}
128
129impl CacheEntry {
130 fn new(data: Bytes) -> Self {
132 let size = data.len();
133 Self {
134 data,
135 created_at: Instant::now(),
136 size,
137 access_count: 0,
138 }
139 }
140
141 fn is_expired(&self, ttl: Duration) -> bool {
143 self.created_at.elapsed() > ttl
144 }
145
146 fn record_access(&mut self) {
148 self.access_count += 1;
149 }
150}
151
152#[derive(Debug, Clone, Default)]
154pub struct CacheStats {
155 pub hits: u64,
157
158 pub misses: u64,
160
161 pub entry_count: usize,
163
164 pub total_size: usize,
166
167 pub evictions: u64,
169
170 pub expirations: u64,
172
173 pub disk_reads: u64,
175
176 pub disk_writes: u64,
178}
179
180impl CacheStats {
181 pub fn hit_rate(&self) -> f64 {
183 let total = self.hits + self.misses;
184 if total == 0 {
185 0.0
186 } else {
187 self.hits as f64 / total as f64
188 }
189 }
190
191 pub fn avg_entry_size(&self) -> f64 {
193 if self.entry_count == 0 {
194 0.0
195 } else {
196 self.total_size as f64 / self.entry_count as f64
197 }
198 }
199}
200
201#[derive(Debug, Clone)]
203pub struct TileCacheConfig {
204 pub max_memory_bytes: usize,
206
207 pub disk_cache_dir: Option<PathBuf>,
209
210 pub ttl: Duration,
212
213 pub enable_stats: bool,
215
216 pub compression: bool,
218}
219
220impl Default for TileCacheConfig {
221 fn default() -> Self {
222 Self {
223 max_memory_bytes: 256 * 1024 * 1024, disk_cache_dir: None,
225 ttl: Duration::from_secs(3600), enable_stats: true,
227 compression: false,
228 }
229 }
230}
231
232pub struct TileCache {
234 memory_cache: Arc<Mutex<LruCache<CacheKey, CacheEntry>>>,
236
237 memory_usage: Arc<Mutex<usize>>,
239
240 config: TileCacheConfig,
242
243 stats: Arc<Mutex<CacheStats>>,
245}
246
247const MIN_CACHE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(100) {
249 Some(n) => n,
250 None => unreachable!(),
251};
252
253impl TileCache {
254 pub fn new(config: TileCacheConfig) -> Self {
256 let estimated_capacity = config.max_memory_bytes / (10 * 1024);
258 let capacity = NonZeroUsize::new(estimated_capacity)
260 .unwrap_or(MIN_CACHE_CAPACITY)
261 .max(MIN_CACHE_CAPACITY);
262
263 Self {
264 memory_cache: Arc::new(Mutex::new(LruCache::new(capacity))),
265 memory_usage: Arc::new(Mutex::new(0)),
266 config,
267 stats: Arc::new(Mutex::new(CacheStats::default())),
268 }
269 }
270
271 pub fn get(&self, key: &CacheKey) -> Option<Bytes> {
273 trace!("Cache lookup: {}", key.to_string());
274
275 if let Some(data) = self.get_from_memory(key) {
277 self.record_hit();
278 return Some(data);
279 }
280
281 if self.config.disk_cache_dir.is_some() {
283 if let Some(data) = self.get_from_disk(key) {
284 let _ = self.put_in_memory(key.clone(), data.clone());
286 self.record_hit();
287 return Some(data);
288 }
289 }
290
291 self.record_miss();
292 None
293 }
294
295 pub fn put(&self, key: CacheKey, data: Bytes) -> CacheResult<()> {
297 trace!("Caching tile: {}", key.to_string());
298
299 self.put_in_memory(key.clone(), data.clone())?;
301
302 if self.config.disk_cache_dir.is_some() {
304 let _ = self.put_on_disk(&key, &data);
305 }
306
307 Ok(())
308 }
309
310 fn get_from_memory(&self, key: &CacheKey) -> Option<Bytes> {
312 let mut cache = self.memory_cache.lock().ok()?;
313
314 let is_expired = if let Some(entry) = cache.peek(key) {
316 entry.is_expired(self.config.ttl)
317 } else {
318 return None;
319 };
320
321 if is_expired {
322 trace!("Entry expired: {}", key.to_string());
323 self.record_expiration();
324 let entry = cache.pop(key)?;
325 self.update_memory_usage(|usage| usage.saturating_sub(entry.size));
326 return None;
327 }
328
329 if let Some(entry) = cache.get_mut(key) {
331 entry.record_access();
332 Some(entry.data.clone())
333 } else {
334 None
335 }
336 }
337
338 fn put_in_memory(&self, key: CacheKey, data: Bytes) -> CacheResult<()> {
340 let entry = CacheEntry::new(data);
341 let entry_size = entry.size;
342
343 let mut cache = self.memory_cache.lock().map_err(|_| CacheError::Full)?;
344
345 while self.get_memory_usage() + entry_size > self.config.max_memory_bytes {
347 if let Some((_, evicted)) = cache.pop_lru() {
348 debug!("Evicting entry from memory cache");
349 self.update_memory_usage(|usage| usage.saturating_sub(evicted.size));
350 self.record_eviction();
351 } else {
352 break;
353 }
354 }
355
356 if let Some(old_entry) = cache.put(key, entry) {
358 self.update_memory_usage(|usage| usage.saturating_sub(old_entry.size));
359 }
360
361 self.update_memory_usage(|usage| usage + entry_size);
362
363 Ok(())
364 }
365
366 fn get_from_disk(&self, key: &CacheKey) -> Option<Bytes> {
368 let base_dir = self.config.disk_cache_dir.as_ref()?;
369 let path = key.to_path(base_dir);
370
371 match std::fs::read(&path) {
372 Ok(data) => {
373 trace!("Disk cache hit: {}", path.display());
374 self.record_disk_read();
375 Some(Bytes::from(data))
376 }
377 Err(_) => None,
378 }
379 }
380
381 fn put_on_disk(&self, key: &CacheKey, data: &Bytes) -> CacheResult<()> {
383 let base_dir =
384 self.config
385 .disk_cache_dir
386 .as_ref()
387 .ok_or(CacheError::Io(std::io::Error::new(
388 std::io::ErrorKind::NotFound,
389 "No disk cache directory",
390 )))?;
391
392 let path = key.to_path(base_dir);
393
394 if let Some(parent) = path.parent() {
396 std::fs::create_dir_all(parent)?;
397 }
398
399 std::fs::write(&path, data)?;
401 self.record_disk_write();
402
403 trace!("Wrote to disk cache: {}", path.display());
404 Ok(())
405 }
406
407 pub fn clear(&self) -> CacheResult<()> {
409 if let Ok(mut cache) = self.memory_cache.lock() {
411 cache.clear();
412 }
413
414 self.update_memory_usage(|_| 0);
415
416 if let Some(ref dir) = self.config.disk_cache_dir {
418 if dir.exists() {
419 std::fs::remove_dir_all(dir)?;
420 std::fs::create_dir_all(dir)?;
421 }
422 }
423
424 if let Ok(mut stats) = self.stats.lock() {
426 *stats = CacheStats::default();
427 }
428
429 debug!("Cache cleared");
430 Ok(())
431 }
432
433 pub fn stats(&self) -> CacheStats {
435 self.stats.lock().map(|s| s.clone()).unwrap_or_default()
436 }
437
438 fn get_memory_usage(&self) -> usize {
440 self.memory_usage.lock().map(|u| *u).unwrap_or(0)
441 }
442
443 fn update_memory_usage<F>(&self, f: F)
445 where
446 F: FnOnce(usize) -> usize,
447 {
448 if let Ok(mut usage) = self.memory_usage.lock() {
449 *usage = f(*usage);
450 }
451
452 if let Ok(mut stats) = self.stats.lock() {
453 stats.total_size = self.get_memory_usage();
454 }
455 }
456
457 fn record_hit(&self) {
459 if self.config.enable_stats {
460 if let Ok(mut stats) = self.stats.lock() {
461 stats.hits += 1;
462 }
463 }
464 }
465
466 fn record_miss(&self) {
468 if self.config.enable_stats {
469 if let Ok(mut stats) = self.stats.lock() {
470 stats.misses += 1;
471 }
472 }
473 }
474
475 fn record_eviction(&self) {
477 if self.config.enable_stats {
478 if let Ok(mut stats) = self.stats.lock() {
479 stats.evictions += 1;
480 }
481 }
482 }
483
484 fn record_expiration(&self) {
486 if self.config.enable_stats {
487 if let Ok(mut stats) = self.stats.lock() {
488 stats.expirations += 1;
489 }
490 }
491 }
492
493 fn record_disk_read(&self) {
495 if self.config.enable_stats {
496 if let Ok(mut stats) = self.stats.lock() {
497 stats.disk_reads += 1;
498 }
499 }
500 }
501
502 fn record_disk_write(&self) {
504 if self.config.enable_stats {
505 if let Ok(mut stats) = self.stats.lock() {
506 stats.disk_writes += 1;
507 }
508 }
509 }
510}
511
512impl Clone for TileCache {
513 fn clone(&self) -> Self {
514 Self {
515 memory_cache: Arc::clone(&self.memory_cache),
516 memory_usage: Arc::clone(&self.memory_usage),
517 config: self.config.clone(),
518 stats: Arc::clone(&self.stats),
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_cache_key_to_string() {
529 let key = CacheKey::new("landsat".to_string(), 10, 512, 384, "png".to_string());
530 assert_eq!(key.to_string(), "landsat/10/512/384.png");
531
532 let key_with_style = key.with_style("default".to_string());
533 assert_eq!(key_with_style.to_string(), "landsat/default/10/512/384.png");
534 }
535
536 #[test]
537 fn test_cache_put_get() {
538 let config = TileCacheConfig::default();
539 let cache = TileCache::new(config);
540
541 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
542 let data = Bytes::from(vec![1, 2, 3, 4, 5]);
543
544 cache.put(key.clone(), data.clone()).expect("put failed");
545
546 let retrieved = cache.get(&key).expect("get failed");
547 assert_eq!(retrieved, data);
548 }
549
550 #[test]
551 fn test_cache_miss() {
552 let config = TileCacheConfig::default();
553 let cache = TileCache::new(config);
554
555 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
556 assert!(cache.get(&key).is_none());
557
558 let stats = cache.stats();
559 assert_eq!(stats.misses, 1);
560 assert_eq!(stats.hits, 0);
561 }
562
563 #[test]
564 fn test_cache_stats() {
565 let config = TileCacheConfig::default();
566 let cache = TileCache::new(config);
567
568 let key1 = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
569 let key2 = CacheKey::new("test".to_string(), 0, 0, 1, "png".to_string());
570 let data = Bytes::from(vec![1, 2, 3]);
571
572 cache.put(key1.clone(), data.clone()).expect("put failed");
573 cache.put(key2.clone(), data.clone()).expect("put failed");
574
575 cache.get(&key1);
576 cache.get(&key2);
577 cache.get(&CacheKey::new(
578 "nonexistent".to_string(),
579 0,
580 0,
581 0,
582 "png".to_string(),
583 ));
584
585 let stats = cache.stats();
586 assert_eq!(stats.hits, 2);
587 assert_eq!(stats.misses, 1);
588 assert!(stats.hit_rate() > 0.6);
589 }
590
591 #[test]
592 fn test_cache_clear() {
593 let config = TileCacheConfig::default();
594 let cache = TileCache::new(config);
595
596 let key = CacheKey::new("test".to_string(), 0, 0, 0, "png".to_string());
597 let data = Bytes::from(vec![1, 2, 3]);
598
599 cache.put(key.clone(), data).expect("put failed");
600 assert!(cache.get(&key).is_some());
601
602 cache.clear().expect("clear failed");
603 assert!(cache.get(&key).is_none());
604 }
605}