Skip to main content

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}