oxigdal_proj/cache.rs
1//! Thread-safe LRU cache for [`Transformer`] instances keyed by
2//! `(src_epsg, dst_epsg)`.
3//!
4//! Building a [`Transformer`] from EPSG codes incurs non-trivial cost:
5//! both source and target [`crate::crs::Crs`] are constructed from the
6//! embedded EPSG database, each is serialised to a PROJ string, and the
7//! underlying `proj4rs::Proj` is initialised twice (source + target). For
8//! workloads that repeatedly transform between the same EPSG pair —
9//! tile pipelines, vector reprojection batches, geocoders — this cost
10//! dominates.
11//!
12//! [`TransformerCache`] provides an opt-in, thread-safe cache with a
13//! bounded number of entries and LRU eviction. Internally it stores
14//! `Arc<Transformer>` so callers may freely clone, share across threads,
15//! or hold the handle for the lifetime of a single transform operation.
16//!
17//! # Concurrency model
18//!
19//! All shared state lives behind a single [`std::sync::Mutex`]. The
20//! cache is designed for short critical sections:
21//!
22//! * **Hit path** locks the mutex, looks up the key, updates the MRU
23//! order, and returns a cheap `Arc` clone.
24//! * **Miss path** drops the lock before invoking
25//! [`Transformer::from_epsg`] (which may perform allocation, PROJ
26//! string parsing, and possibly trigger I/O when geoid grids are
27//! later wired in), then re-acquires the lock to insert. A second
28//! thread that races in and inserts the same key first wins; the
29//! loser's freshly-built `Transformer` is dropped and the cached
30//! instance is returned. This keeps the cache strictly single-copy.
31//!
32//! # Eviction
33//!
34//! Eviction is least-recently-used: every successful `get_or_build`
35//! call promotes the key to the front of the order queue, and inserts
36//! beyond `capacity` evict from the back. `capacity` is clamped to at
37//! least 1 to keep the invariant "the most recently inserted entry is
38//! always present" trivially true.
39//!
40//! # Example
41//!
42//! ```no_run
43//! use oxigdal_proj::TransformerCache;
44//!
45//! let cache = TransformerCache::new(16);
46//! let wgs84_to_web = cache
47//! .get_or_build(4326, 3857)
48//! .expect("EPSG:4326→3857 must build");
49//!
50//! // Subsequent calls return the same Arc (no rebuild):
51//! let again = cache
52//! .get_or_build(4326, 3857)
53//! .expect("cached");
54//! assert!(std::sync::Arc::ptr_eq(&wgs84_to_web, &again));
55//! ```
56
57use std::collections::{HashMap, VecDeque};
58use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
59
60use crate::error::Error;
61use crate::transform::Transformer;
62
63/// Acquires the inner mutex, transparently recovering from poisoning.
64///
65/// A poisoned lock means a previous holder panicked while mutating the
66/// cache state. Because the cache is purely a memoisation layer (its
67/// state can be safely re-derived), continuing with the inner data is
68/// always sound — there is no invariant that a panic could have
69/// half-violated. Returning the inner guard avoids both `expect`/
70/// `unwrap` (forbidden by clippy lints) and surprise panics in user
71/// code that simply needs to read a few cache stats.
72fn lock_inner(mutex: &Mutex<LruInner>) -> MutexGuard<'_, LruInner> {
73 mutex.lock().unwrap_or_else(PoisonError::into_inner)
74}
75
76/// Composite cache key identifying a `(src_epsg, dst_epsg)` transformer
77/// pair.
78///
79/// The struct is `Copy` + `Eq` + `Hash` so it can be used directly in a
80/// [`HashMap`] and trivially moved through the MRU queue.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct TransformerKey {
83 /// Source EPSG code (e.g. `4326` for WGS84).
84 pub src_epsg: u32,
85 /// Destination EPSG code (e.g. `3857` for Web Mercator).
86 pub dst_epsg: u32,
87}
88
89impl TransformerKey {
90 /// Convenience constructor.
91 pub fn new(src_epsg: u32, dst_epsg: u32) -> Self {
92 Self { src_epsg, dst_epsg }
93 }
94}
95
96/// Inner LRU state guarded by [`TransformerCache`]'s mutex.
97///
98/// `map` owns the cached `Arc<Transformer>` instances; `order` records
99/// the MRU sequence, with the **front** being most-recently-used and
100/// the **back** being the next eviction candidate. Both collections
101/// are kept consistent: every key in `map` appears exactly once in
102/// `order` and vice versa.
103struct LruInner {
104 /// Owning storage of cached transformer Arcs.
105 map: HashMap<TransformerKey, Arc<Transformer>>,
106 /// MRU-first key order. `front` = newest, `back` = oldest.
107 order: VecDeque<TransformerKey>,
108}
109
110impl LruInner {
111 /// Promotes `key` to the front of `order`. Assumes `key` already
112 /// exists in the queue (it is removed, then re-pushed at the front).
113 /// If the key is missing, this is a no-op (caller bug).
114 fn touch(&mut self, key: &TransformerKey) {
115 if let Some(pos) = self.order.iter().position(|k| k == key) {
116 self.order.remove(pos);
117 }
118 self.order.push_front(*key);
119 }
120}
121
122/// Thread-safe LRU cache for [`Transformer`] instances.
123///
124/// `TransformerCache` is cheap to clone — internally a single
125/// `Arc<Mutex<…>>` is shared. All clones see the same cache; entries
126/// inserted through one handle are visible through every other.
127///
128/// Lookups, insertions, and eviction all run under a single mutex.
129/// The mutex is **dropped while a fresh transformer is being built**,
130/// so a single slow build does not block parallel hits for other
131/// EPSG pairs.
132#[derive(Clone)]
133pub struct TransformerCache {
134 inner: Arc<Mutex<LruInner>>,
135 capacity: usize,
136}
137
138impl TransformerCache {
139 /// Constructs a new cache with at most `capacity` cached entries.
140 ///
141 /// `capacity` is clamped to a minimum of 1: a zero-capacity cache
142 /// would be useless (every call would have to rebuild from
143 /// scratch), so the clamp gracefully handles invalid input rather
144 /// than panicking.
145 pub fn new(capacity: usize) -> Self {
146 let capacity = capacity.max(1);
147 Self {
148 inner: Arc::new(Mutex::new(LruInner {
149 map: HashMap::with_capacity(capacity),
150 order: VecDeque::with_capacity(capacity),
151 })),
152 capacity,
153 }
154 }
155
156 /// Returns the effective capacity (post-clamp).
157 pub fn capacity(&self) -> usize {
158 self.capacity
159 }
160
161 /// Returns the number of entries currently cached.
162 pub fn len(&self) -> usize {
163 let guard = lock_inner(&self.inner);
164 guard.map.len()
165 }
166
167 /// Returns `true` when the cache holds no entries.
168 pub fn is_empty(&self) -> bool {
169 self.len() == 0
170 }
171
172 /// Returns `true` when the given EPSG pair is currently cached.
173 ///
174 /// This does **not** promote the key in the MRU order — it is a
175 /// pure read query intended for introspection / metrics / tests.
176 pub fn contains(&self, src_epsg: u32, dst_epsg: u32) -> bool {
177 let guard = lock_inner(&self.inner);
178 guard
179 .map
180 .contains_key(&TransformerKey::new(src_epsg, dst_epsg))
181 }
182
183 /// Removes all cached entries. Capacity is preserved.
184 pub fn clear(&self) {
185 let mut guard = lock_inner(&self.inner);
186 guard.map.clear();
187 guard.order.clear();
188 }
189
190 /// Returns a snapshot of cached keys in MRU order
191 /// (front = newest, back = oldest).
192 ///
193 /// Intended for introspection and tests: the cache is locked only
194 /// briefly to copy the queue, then released before the snapshot is
195 /// returned, so the result is a point-in-time view that may go
196 /// stale immediately.
197 pub fn cached_keys(&self) -> Vec<TransformerKey> {
198 let guard = lock_inner(&self.inner);
199 guard.order.iter().copied().collect()
200 }
201
202 /// Returns a cached transformer for the `(src_epsg, dst_epsg)`
203 /// pair, building and inserting one on miss.
204 ///
205 /// # Behaviour
206 ///
207 /// * **Hit** — the cached `Arc` is cloned and returned; the key is
208 /// promoted to the front of the MRU order.
209 /// * **Miss** — the cache mutex is released, a fresh
210 /// [`Transformer::from_epsg`] is constructed, then the mutex is
211 /// re-acquired and the entry is inserted. If another thread
212 /// raced in and inserted first, the freshly built transformer is
213 /// dropped and the already-cached `Arc` is returned, preserving
214 /// single-copy semantics.
215 /// * **Eviction** — when at `capacity`, the back of the MRU queue
216 /// is removed before the new entry is inserted.
217 ///
218 /// # Errors
219 ///
220 /// Propagates any error from [`Transformer::from_epsg`] — typically
221 /// [`Error::EpsgCodeNotFound`] for unknown codes, or
222 /// [`Error::ProjectionInitError`] when the underlying `proj4rs`
223 /// initialisation fails.
224 pub fn get_or_build(&self, src_epsg: u32, dst_epsg: u32) -> Result<Arc<Transformer>, Error> {
225 let key = TransformerKey::new(src_epsg, dst_epsg);
226
227 // Fast path: hit. Take the lock, look up the Arc, promote the
228 // key to MRU front, and return. No transformer construction
229 // happens while the lock is held in this path.
230 {
231 let mut guard = lock_inner(&self.inner);
232 if let Some(arc) = guard.map.get(&key).cloned() {
233 guard.touch(&key);
234 return Ok(arc);
235 }
236 }
237
238 // Slow path: miss. Build outside the lock so other threads
239 // hitting unrelated keys are not blocked while proj4rs parses
240 // PROJ strings and initialises projection objects.
241 let transformer = Arc::new(Transformer::from_epsg(src_epsg, dst_epsg)?);
242
243 // Insert under lock. Another thread may have raced in and
244 // inserted the same key while we were building — re-check
245 // before committing our own instance so the cache never
246 // contains two distinct Arcs for the same key.
247 let mut guard = lock_inner(&self.inner);
248 if let Some(arc) = guard.map.get(&key).cloned() {
249 // Lost the race; promote MRU and discard our freshly built
250 // transformer. Returning the existing Arc preserves the
251 // single-copy invariant — every concurrent caller for the
252 // same key observes the **same** Transformer instance.
253 guard.touch(&key);
254 return Ok(arc);
255 }
256
257 // Evict until there is room for the new entry. The loop
258 // tolerates a degenerate empty `order` (which would only
259 // happen under a programming error elsewhere) by breaking
260 // cleanly rather than spinning forever.
261 while guard.map.len() >= self.capacity {
262 match guard.order.pop_back() {
263 Some(oldest) => {
264 guard.map.remove(&oldest);
265 }
266 None => break,
267 }
268 }
269
270 guard.map.insert(key, transformer.clone());
271 guard.order.push_front(key);
272 Ok(transformer)
273 }
274}
275
276impl std::fmt::Debug for TransformerCache {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 // Take a short read-only snapshot to render len without holding
279 // the lock across the formatter machinery itself.
280 let len = {
281 let guard = lock_inner(&self.inner);
282 guard.map.len()
283 };
284 f.debug_struct("TransformerCache")
285 .field("len", &len)
286 .field("capacity", &self.capacity)
287 .finish()
288 }
289}
290
291#[cfg(test)]
292#[allow(clippy::expect_used)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_key_equality_and_hash() {
298 let k1 = TransformerKey::new(4326, 3857);
299 let k2 = TransformerKey::new(4326, 3857);
300 let k3 = TransformerKey::new(3857, 4326);
301 assert_eq!(k1, k2);
302 assert_ne!(k1, k3);
303
304 // Verify hash-equality contract is satisfied for HashMap usage.
305 let mut map: HashMap<TransformerKey, i32> = HashMap::new();
306 map.insert(k1, 42);
307 assert_eq!(map.get(&k2), Some(&42));
308 assert_eq!(map.get(&k3), None);
309 }
310
311 #[test]
312 fn test_capacity_clamp() {
313 let c0 = TransformerCache::new(0);
314 let c5 = TransformerCache::new(5);
315 assert_eq!(c0.capacity(), 1);
316 assert_eq!(c5.capacity(), 5);
317 }
318
319 #[test]
320 fn test_debug_format() {
321 let cache = TransformerCache::new(4);
322 let s = format!("{:?}", cache);
323 assert!(s.contains("TransformerCache"));
324 assert!(s.contains("len"));
325 assert!(s.contains("capacity"));
326 }
327}