Skip to main content

oxigdal_streaming/tile/
cache.rs

1//! Tile caching for improved performance.
2
3use super::protocol::{TileCoordinate, TileResponse};
4use crate::error::{Result, StreamingError};
5use dashmap::DashMap;
6use lru::LruCache;
7use std::num::NonZeroUsize;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::fs;
11use tokio::sync::RwLock;
12use tracing::debug;
13
14/// Configuration for tile cache.
15#[derive(Debug, Clone)]
16pub struct TileCacheConfig {
17    /// Maximum number of tiles in memory cache
18    pub max_memory_tiles: usize,
19
20    /// Maximum size of disk cache in bytes
21    pub max_disk_bytes: u64,
22
23    /// Directory for disk cache
24    pub disk_cache_dir: Option<PathBuf>,
25
26    /// Enable compression for disk cache
27    pub compress: bool,
28
29    /// TTL for cached tiles in seconds
30    pub ttl_seconds: u64,
31}
32
33impl Default for TileCacheConfig {
34    fn default() -> Self {
35        Self {
36            max_memory_tiles: 1000,
37            max_disk_bytes: 1024 * 1024 * 1024, // 1GB
38            disk_cache_dir: None,
39            compress: false,
40            ttl_seconds: 3600, // 1 hour
41        }
42    }
43}
44
45/// Tile cache implementation.
46pub struct TileCache {
47    config: TileCacheConfig,
48    memory_cache: Arc<RwLock<LruCache<TileCoordinate, CachedTile>>>,
49    disk_cache_map: Arc<DashMap<TileCoordinate, PathBuf>>,
50}
51
52struct CachedTile {
53    response: TileResponse,
54    cached_at: std::time::Instant,
55}
56
57impl TileCache {
58    /// Create a new tile cache.
59    pub fn new(config: TileCacheConfig) -> Result<Self> {
60        let max_size = NonZeroUsize::new(config.max_memory_tiles)
61            .ok_or_else(|| StreamingError::ConfigError("Invalid cache size".to_string()))?;
62
63        Ok(Self {
64            config,
65            memory_cache: Arc::new(RwLock::new(LruCache::new(max_size))),
66            disk_cache_map: Arc::new(DashMap::new()),
67        })
68    }
69
70    /// Get a tile from cache.
71    pub async fn get(&self, coord: &TileCoordinate) -> Option<TileResponse> {
72        // Check memory cache
73        let mut cache = self.memory_cache.write().await;
74        if let Some(cached) = cache.get(coord)
75            && !self.is_expired(&cached.cached_at)
76        {
77            debug!("Memory cache hit for tile {}", coord);
78            return Some(cached.response.clone());
79        }
80        drop(cache);
81
82        // Check disk cache
83        if let Some(path) = self.disk_cache_map.get(coord)
84            && let Ok(response) = self.load_from_disk(coord, path.value()).await
85        {
86            debug!("Disk cache hit for tile {}", coord);
87            // Promote to memory cache
88            self.put_memory(coord, response.clone()).await.ok();
89            return Some(response);
90        }
91
92        None
93    }
94
95    /// Put a tile in cache.
96    pub async fn put(&self, response: TileResponse) -> Result<()> {
97        let coord = response.coord;
98
99        // Store in memory cache
100        self.put_memory(&coord, response.clone()).await?;
101
102        // Store in disk cache if enabled
103        if self.config.disk_cache_dir.is_some() {
104            self.put_disk(&coord, response).await?;
105        }
106
107        Ok(())
108    }
109
110    /// Put a tile in memory cache.
111    async fn put_memory(&self, coord: &TileCoordinate, response: TileResponse) -> Result<()> {
112        let mut cache = self.memory_cache.write().await;
113        cache.put(
114            *coord,
115            CachedTile {
116                response,
117                cached_at: std::time::Instant::now(),
118            },
119        );
120        Ok(())
121    }
122
123    /// Put a tile in disk cache.
124    async fn put_disk(&self, coord: &TileCoordinate, response: TileResponse) -> Result<()> {
125        let cache_dir =
126            self.config.disk_cache_dir.as_ref().ok_or_else(|| {
127                StreamingError::ConfigError("Disk cache not configured".to_string())
128            })?;
129
130        let path = self.tile_path(cache_dir, coord);
131
132        // Create parent directory
133        if let Some(parent) = path.parent() {
134            fs::create_dir_all(parent)
135                .await
136                .map_err(StreamingError::Io)?;
137        }
138
139        // Write tile data
140        fs::write(&path, &response.data)
141            .await
142            .map_err(StreamingError::Io)?;
143
144        self.disk_cache_map.insert(*coord, path);
145
146        Ok(())
147    }
148
149    /// Load a tile from disk cache.
150    async fn load_from_disk(&self, coord: &TileCoordinate, path: &Path) -> Result<TileResponse> {
151        let data = fs::read(path).await.map_err(StreamingError::Io)?;
152
153        Ok(TileResponse::new(
154            *coord,
155            bytes::Bytes::from(data),
156            "image/png".to_string(),
157        ))
158    }
159
160    /// Get the file path for a tile.
161    fn tile_path(&self, base_dir: &Path, coord: &TileCoordinate) -> PathBuf {
162        base_dir.join(format!("{}/{}/{}.png", coord.z, coord.x, coord.y))
163    }
164
165    /// Check if a cached tile is expired.
166    fn is_expired(&self, cached_at: &std::time::Instant) -> bool {
167        cached_at.elapsed().as_secs() > self.config.ttl_seconds
168    }
169
170    /// Clear all caches.
171    pub async fn clear(&self) -> Result<()> {
172        let mut cache = self.memory_cache.write().await;
173        cache.clear();
174        drop(cache);
175
176        self.disk_cache_map.clear();
177
178        if let Some(cache_dir) = &self.config.disk_cache_dir
179            && cache_dir.exists()
180        {
181            fs::remove_dir_all(cache_dir)
182                .await
183                .map_err(StreamingError::Io)?;
184        }
185
186        Ok(())
187    }
188
189    /// Get cache statistics.
190    pub async fn stats(&self) -> CacheStats {
191        let cache = self.memory_cache.read().await;
192        CacheStats {
193            memory_tiles: cache.len(),
194            disk_tiles: self.disk_cache_map.len(),
195            max_memory_tiles: self.config.max_memory_tiles,
196        }
197    }
198}
199
200/// Cache statistics.
201#[derive(Debug, Clone)]
202pub struct CacheStats {
203    /// Number of tiles in memory cache
204    pub memory_tiles: usize,
205
206    /// Number of tiles in disk cache
207    pub disk_tiles: usize,
208
209    /// Maximum memory cache size
210    pub max_memory_tiles: usize,
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use bytes::Bytes;
217
218    #[tokio::test]
219    async fn test_memory_cache() {
220        let config = TileCacheConfig {
221            max_memory_tiles: 10,
222            ..Default::default()
223        };
224
225        let cache = TileCache::new(config).ok();
226        assert!(cache.is_some());
227
228        if let Some(cache) = cache {
229            let coord = TileCoordinate::new(10, 512, 384);
230            let response =
231                TileResponse::new(coord, Bytes::from(vec![0u8; 1024]), "image/png".to_string());
232
233            cache.put(response).await.ok();
234
235            let retrieved = cache.get(&coord).await;
236            assert!(retrieved.is_some());
237        }
238    }
239}