Skip to main content

oxigdal_server/
cache.rs

1//! Tile caching system
2//!
3//! Provides multi-level caching for rendered tiles:
4//! - In-memory LRU cache for fast access
5//! - Optional disk cache for persistence
6//! - Cache statistics and monitoring
7
8use 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/// Cache errors
20#[derive(Debug, Error)]
21pub enum CacheError {
22    /// I/O error
23    #[error("Cache I/O error: {0}")]
24    Io(#[from] std::io::Error),
25
26    /// Invalid cache key
27    #[error("Invalid cache key")]
28    InvalidKey,
29
30    /// Cache is full
31    #[error("Cache is full")]
32    Full,
33}
34
35/// Result type for cache operations
36pub type CacheResult<T> = Result<T, CacheError>;
37
38/// Cache key for tiles
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct CacheKey {
41    /// Layer name
42    pub layer: String,
43
44    /// Zoom level
45    pub z: u8,
46
47    /// Tile X coordinate
48    pub x: u32,
49
50    /// Tile Y coordinate
51    pub y: u32,
52
53    /// Image format extension (png, jpg, webp)
54    pub format: String,
55
56    /// Optional style name
57    pub style: Option<String>,
58}
59
60impl CacheKey {
61    /// Create a new cache key
62    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    /// Create a cache key with style
74    pub fn with_style(mut self, style: String) -> Self {
75        self.style = Some(style);
76        self
77    }
78
79    /// Get the file path for disk cache
80    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/// Cached tile entry
114#[derive(Debug, Clone)]
115struct CacheEntry {
116    /// Tile data
117    data: Bytes,
118
119    /// When this entry was created
120    created_at: Instant,
121
122    /// Entry size in bytes
123    size: usize,
124
125    /// Access count
126    access_count: u64,
127}
128
129impl CacheEntry {
130    /// Create a new cache entry
131    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    /// Check if this entry has expired
142    fn is_expired(&self, ttl: Duration) -> bool {
143        self.created_at.elapsed() > ttl
144    }
145
146    /// Record an access
147    fn record_access(&mut self) {
148        self.access_count += 1;
149    }
150}
151
152/// Cache statistics
153#[derive(Debug, Clone, Default)]
154pub struct CacheStats {
155    /// Number of cache hits
156    pub hits: u64,
157
158    /// Number of cache misses
159    pub misses: u64,
160
161    /// Total number of entries
162    pub entry_count: usize,
163
164    /// Total size in bytes
165    pub total_size: usize,
166
167    /// Number of evictions
168    pub evictions: u64,
169
170    /// Number of expirations
171    pub expirations: u64,
172
173    /// Number of disk reads
174    pub disk_reads: u64,
175
176    /// Number of disk writes
177    pub disk_writes: u64,
178}
179
180impl CacheStats {
181    /// Calculate hit rate
182    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    /// Calculate average entry size
192    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/// Tile cache configuration
202#[derive(Debug, Clone)]
203pub struct TileCacheConfig {
204    /// Maximum memory size in bytes
205    pub max_memory_bytes: usize,
206
207    /// Optional disk cache directory
208    pub disk_cache_dir: Option<PathBuf>,
209
210    /// Time-to-live for cached entries
211    pub ttl: Duration,
212
213    /// Enable statistics tracking
214    pub enable_stats: bool,
215
216    /// Compress cached data
217    pub compression: bool,
218}
219
220impl Default for TileCacheConfig {
221    fn default() -> Self {
222        Self {
223            max_memory_bytes: 256 * 1024 * 1024, // 256 MB
224            disk_cache_dir: None,
225            ttl: Duration::from_secs(3600), // 1 hour
226            enable_stats: true,
227            compression: false,
228        }
229    }
230}
231
232/// Multi-level tile cache
233pub struct TileCache {
234    /// In-memory LRU cache
235    memory_cache: Arc<Mutex<LruCache<CacheKey, CacheEntry>>>,
236
237    /// Current memory usage
238    memory_usage: Arc<Mutex<usize>>,
239
240    /// Cache configuration
241    config: TileCacheConfig,
242
243    /// Cache statistics
244    stats: Arc<Mutex<CacheStats>>,
245}
246
247/// Minimum cache capacity (100 entries)
248const MIN_CACHE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(100) {
249    Some(n) => n,
250    None => unreachable!(),
251};
252
253impl TileCache {
254    /// Create a new tile cache
255    pub fn new(config: TileCacheConfig) -> Self {
256        // Calculate capacity based on assumed average tile size of 10KB
257        let estimated_capacity = config.max_memory_bytes / (10 * 1024);
258        // Use min capacity of 100, max of estimated_capacity
259        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    /// Get a tile from the cache
272    pub fn get(&self, key: &CacheKey) -> Option<Bytes> {
273        trace!("Cache lookup: {}", key.to_string());
274
275        // Try memory cache first
276        if let Some(data) = self.get_from_memory(key) {
277            self.record_hit();
278            return Some(data);
279        }
280
281        // Try disk cache if enabled
282        if self.config.disk_cache_dir.is_some() {
283            if let Some(data) = self.get_from_disk(key) {
284                // Promote to memory cache
285                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    /// Put a tile in the cache
296    pub fn put(&self, key: CacheKey, data: Bytes) -> CacheResult<()> {
297        trace!("Caching tile: {}", key.to_string());
298
299        // Store in memory cache
300        self.put_in_memory(key.clone(), data.clone())?;
301
302        // Store in disk cache if enabled
303        if self.config.disk_cache_dir.is_some() {
304            let _ = self.put_on_disk(&key, &data);
305        }
306
307        Ok(())
308    }
309
310    /// Get from memory cache
311    fn get_from_memory(&self, key: &CacheKey) -> Option<Bytes> {
312        let mut cache = self.memory_cache.lock().ok()?;
313
314        // Check if entry exists and is not expired
315        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        // Entry exists and is not expired - get it and record access
330        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    /// Put in memory cache
339    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        // Evict entries if necessary
346        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        // Insert new entry
357        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    /// Get from disk cache
367    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    /// Put on disk cache
382    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        // Create parent directories
395        if let Some(parent) = path.parent() {
396            std::fs::create_dir_all(parent)?;
397        }
398
399        // Write tile to disk
400        std::fs::write(&path, data)?;
401        self.record_disk_write();
402
403        trace!("Wrote to disk cache: {}", path.display());
404        Ok(())
405    }
406
407    /// Clear all cached entries
408    pub fn clear(&self) -> CacheResult<()> {
409        // Clear memory cache
410        if let Ok(mut cache) = self.memory_cache.lock() {
411            cache.clear();
412        }
413
414        self.update_memory_usage(|_| 0);
415
416        // Clear disk cache if enabled
417        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        // Reset stats
425        if let Ok(mut stats) = self.stats.lock() {
426            *stats = CacheStats::default();
427        }
428
429        debug!("Cache cleared");
430        Ok(())
431    }
432
433    /// Get cache statistics
434    pub fn stats(&self) -> CacheStats {
435        self.stats.lock().map(|s| s.clone()).unwrap_or_default()
436    }
437
438    /// Get current memory usage
439    fn get_memory_usage(&self) -> usize {
440        self.memory_usage.lock().map(|u| *u).unwrap_or(0)
441    }
442
443    /// Update memory usage
444    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    /// Record a cache hit
458    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    /// Record a cache miss
467    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    /// Record an eviction
476    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    /// Record an expiration
485    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    /// Record a disk read
494    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    /// Record a disk write
503    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}