oxigdal_streaming/tile/
cache.rs1use 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#[derive(Debug, Clone)]
16pub struct TileCacheConfig {
17 pub max_memory_tiles: usize,
19
20 pub max_disk_bytes: u64,
22
23 pub disk_cache_dir: Option<PathBuf>,
25
26 pub compress: bool,
28
29 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, disk_cache_dir: None,
39 compress: false,
40 ttl_seconds: 3600, }
42 }
43}
44
45pub 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 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 pub async fn get(&self, coord: &TileCoordinate) -> Option<TileResponse> {
72 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 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 self.put_memory(coord, response.clone()).await.ok();
89 return Some(response);
90 }
91
92 None
93 }
94
95 pub async fn put(&self, response: TileResponse) -> Result<()> {
97 let coord = response.coord;
98
99 self.put_memory(&coord, response.clone()).await?;
101
102 if self.config.disk_cache_dir.is_some() {
104 self.put_disk(&coord, response).await?;
105 }
106
107 Ok(())
108 }
109
110 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 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 if let Some(parent) = path.parent() {
134 fs::create_dir_all(parent)
135 .await
136 .map_err(StreamingError::Io)?;
137 }
138
139 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 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 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 fn is_expired(&self, cached_at: &std::time::Instant) -> bool {
167 cached_at.elapsed().as_secs() > self.config.ttl_seconds
168 }
169
170 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 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#[derive(Debug, Clone)]
202pub struct CacheStats {
203 pub memory_tiles: usize,
205
206 pub disk_tiles: usize,
208
209 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}