Skip to main content

tower_resilience_cache/
config.rs

1//! Configuration for cache.
2
3use crate::events::CacheEvent;
4use crate::eviction::EvictionPolicy;
5use std::hash::Hash;
6use std::sync::Arc;
7use std::time::Duration;
8use tower_resilience_core::{EventListeners, FnListener};
9
10/// Function that extracts a cache key from a request.
11pub type KeyExtractor<Req, K> = Arc<dyn Fn(&Req) -> K + Send + Sync>;
12
13/// Configuration for the cache pattern.
14pub struct CacheConfig<Req, K> {
15    pub(crate) max_size: usize,
16    pub(crate) ttl: Option<Duration>,
17    pub(crate) eviction_policy: EvictionPolicy,
18    pub(crate) key_extractor: KeyExtractor<Req, K>,
19    pub(crate) event_listeners: EventListeners<CacheEvent>,
20    pub(crate) name: String,
21}
22
23/// Builder for configuring and constructing a cache.
24pub struct CacheConfigBuilder<Req, K> {
25    max_size: usize,
26    ttl: Option<Duration>,
27    eviction_policy: EvictionPolicy,
28    key_extractor: Option<KeyExtractor<Req, K>>,
29    event_listeners: EventListeners<CacheEvent>,
30    name: String,
31}
32
33impl<Req, K> CacheConfigBuilder<Req, K>
34where
35    K: Hash + Eq + Clone + Send + 'static,
36{
37    /// Creates a new builder with default values.
38    pub fn new() -> Self {
39        Self {
40            max_size: 100,
41            ttl: None,
42            eviction_policy: EvictionPolicy::default(),
43            key_extractor: None,
44            event_listeners: EventListeners::new(),
45            name: String::from("<unnamed>"),
46        }
47    }
48
49    /// Sets the maximum number of entries in the cache.
50    ///
51    /// Default: 100
52    pub fn max_size(mut self, size: usize) -> Self {
53        self.max_size = size;
54        self
55    }
56
57    /// Sets the time-to-live for cached entries.
58    ///
59    /// If set, entries will expire after the specified duration.
60    /// Default: None (no expiration)
61    pub fn ttl(mut self, ttl: Duration) -> Self {
62        self.ttl = Some(ttl);
63        self
64    }
65
66    /// Sets the eviction policy for the cache.
67    ///
68    /// Determines which entry to evict when the cache reaches capacity.
69    ///
70    /// # Options
71    ///
72    /// - `EvictionPolicy::Lru` - Least Recently Used (default)
73    ///   - Evicts entries that haven't been accessed recently
74    ///   - Best for general-purpose caching
75    ///
76    /// - `EvictionPolicy::Lfu` - Least Frequently Used
77    ///   - Evicts entries with the lowest access count
78    ///   - Best for long-lived caches with consistently popular items
79    ///
80    /// - `EvictionPolicy::Fifo` - First In, First Out
81    ///   - Evicts the oldest entry regardless of access pattern
82    ///   - Best for time-based caching where age matters
83    ///
84    /// # Example
85    ///
86    /// ```rust
87    /// use tower_resilience_cache::{CacheLayer, EvictionPolicy};
88    ///
89    /// let cache = CacheLayer::<String, String>::builder()
90    ///     .max_size(100)
91    ///     .eviction_policy(EvictionPolicy::Lfu)
92    ///     .key_extractor(|req| req.clone())
93    ///     .build()
94    ///     .unwrap();
95    /// ```
96    ///
97    /// Default: `EvictionPolicy::Lru`
98    pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
99        self.eviction_policy = policy;
100        self
101    }
102
103    /// Sets the function that extracts a cache key from a request.
104    ///
105    /// This function must be provided before building.
106    pub fn key_extractor<F>(mut self, f: F) -> Self
107    where
108        F: Fn(&Req) -> K + Send + Sync + 'static,
109    {
110        self.key_extractor = Some(Arc::new(f));
111        self
112    }
113
114    /// Sets the name of this cache instance for observability.
115    ///
116    /// Default: `"<unnamed>"`
117    pub fn name(mut self, name: impl Into<String>) -> Self {
118        self.name = name.into();
119        self
120    }
121
122    /// Registers a callback when a cache hit occurs.
123    ///
124    /// A cache hit occurs when a requested entry is found in the cache and has not expired.
125    ///
126    /// # Callback Signature
127    /// `Fn()` - Called with no parameters when a cache hit is detected.
128    ///
129    /// # Example
130    /// ```rust,no_run
131    /// use tower_resilience_cache::CacheLayer;
132    /// use std::sync::atomic::{AtomicUsize, Ordering};
133    /// use std::sync::Arc;
134    ///
135    /// #[derive(Clone, Hash, Eq, PartialEq)]
136    /// struct Request {
137    ///     id: String,
138    /// }
139    ///
140    /// let hit_count = Arc::new(AtomicUsize::new(0));
141    /// let counter = Arc::clone(&hit_count);
142    ///
143    /// let config = CacheLayer::<Request, String>::builder()
144    ///     .key_extractor(|req| req.id.clone())
145    ///     .on_hit(move || {
146    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
147    ///         println!("Cache hit #{}", count + 1);
148    ///     })
149    ///     .build()
150    ///     .unwrap();
151    /// ```
152    pub fn on_hit<F>(mut self, f: F) -> Self
153    where
154        F: Fn() + Send + Sync + 'static,
155    {
156        self.event_listeners.add(FnListener::new(move |event| {
157            if matches!(event, CacheEvent::Hit { .. }) {
158                f();
159            }
160        }));
161        self
162    }
163
164    /// Registers a callback when a cache miss occurs.
165    ///
166    /// A cache miss occurs when a requested entry is not found in the cache or has expired.
167    /// The underlying service will be called to fetch the value, which will then be cached.
168    ///
169    /// # Callback Signature
170    /// `Fn()` - Called with no parameters when a cache miss is detected.
171    ///
172    /// # Example
173    /// ```rust,no_run
174    /// use tower_resilience_cache::CacheLayer;
175    /// use std::sync::atomic::{AtomicUsize, Ordering};
176    /// use std::sync::Arc;
177    ///
178    /// #[derive(Clone, Hash, Eq, PartialEq)]
179    /// struct Request {
180    ///     id: String,
181    /// }
182    ///
183    /// let miss_count = Arc::new(AtomicUsize::new(0));
184    /// let counter = Arc::clone(&miss_count);
185    ///
186    /// let config = CacheLayer::<Request, String>::builder()
187    ///     .key_extractor(|req| req.id.clone())
188    ///     .on_miss(move || {
189    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
190    ///         println!("Cache miss #{} - fetching from service", count + 1);
191    ///     })
192    ///     .build()
193    ///     .unwrap();
194    /// ```
195    pub fn on_miss<F>(mut self, f: F) -> Self
196    where
197        F: Fn() + Send + Sync + 'static,
198    {
199        self.event_listeners.add(FnListener::new(move |event| {
200            if matches!(event, CacheEvent::Miss { .. }) {
201                f();
202            }
203        }));
204        self
205    }
206
207    /// Registers a callback when an entry is evicted from the cache.
208    ///
209    /// Eviction occurs when:
210    /// - The cache reaches its maximum size and needs to make room for new entries
211    /// - An entry expires due to TTL (time-to-live) configuration
212    ///
213    /// # Callback Signature
214    /// `Fn()` - Called with no parameters when a cache eviction occurs.
215    ///
216    /// # Example
217    /// ```rust,no_run
218    /// use tower_resilience_cache::CacheLayer;
219    /// use std::sync::atomic::{AtomicUsize, Ordering};
220    /// use std::sync::Arc;
221    /// use std::time::Duration;
222    ///
223    /// #[derive(Clone, Hash, Eq, PartialEq)]
224    /// struct Request {
225    ///     id: String,
226    /// }
227    ///
228    /// let eviction_count = Arc::new(AtomicUsize::new(0));
229    /// let counter = Arc::clone(&eviction_count);
230    ///
231    /// let config = CacheLayer::<Request, String>::builder()
232    ///     .key_extractor(|req| req.id.clone())
233    ///     .max_size(100)
234    ///     .ttl(Duration::from_secs(300))
235    ///     .on_eviction(move || {
236    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
237    ///         println!("Entry evicted (total: {})", count + 1);
238    ///     })
239    ///     .build()
240    ///     .unwrap();
241    /// ```
242    pub fn on_eviction<F>(mut self, f: F) -> Self
243    where
244        F: Fn() + Send + Sync + 'static,
245    {
246        self.event_listeners.add(FnListener::new(move |event| {
247            if matches!(event, CacheEvent::Eviction { .. }) {
248                f();
249            }
250        }));
251        self
252    }
253
254    /// Builds the cache layer.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`crate::CacheBuildError::MissingKeyExtractor`] if `key_extractor`
259    /// was not set before calling `build()`.
260    pub fn build(self) -> Result<crate::CacheLayer<Req, K>, crate::CacheBuildError> {
261        let key_extractor = self
262            .key_extractor
263            .ok_or(crate::CacheBuildError::MissingKeyExtractor)?;
264
265        let config = CacheConfig {
266            max_size: self.max_size,
267            ttl: self.ttl,
268            eviction_policy: self.eviction_policy,
269            key_extractor,
270            event_listeners: self.event_listeners,
271            name: self.name,
272        };
273
274        Ok(crate::CacheLayer::new(config))
275    }
276}
277
278impl<Req, K> Default for CacheConfigBuilder<Req, K>
279where
280    K: Hash + Eq + Clone + Send + 'static,
281{
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::CacheLayer;
291
292    #[derive(Clone, Hash, Eq, PartialEq)]
293    struct TestRequest {
294        id: String,
295    }
296
297    #[test]
298    fn test_builder_defaults() {
299        let _layer = CacheLayer::<TestRequest, String>::builder()
300            .key_extractor(|req| req.id.clone())
301            .build()
302            .unwrap();
303        // If this compiles and doesn't panic, the builder works
304    }
305
306    #[test]
307    fn test_builder_custom_values() {
308        let _layer = CacheLayer::<TestRequest, String>::builder()
309            .max_size(500)
310            .ttl(Duration::from_secs(60))
311            .key_extractor(|req| req.id.clone())
312            .name("my-cache")
313            .build()
314            .unwrap();
315        // If this compiles and doesn't panic, the builder works
316    }
317
318    #[test]
319    fn test_event_listeners() {
320        let _layer = CacheLayer::<TestRequest, String>::builder()
321            .key_extractor(|req| req.id.clone())
322            .on_hit(|| {})
323            .on_miss(|| {})
324            .on_eviction(|| {})
325            .build()
326            .unwrap();
327        // If this compiles and doesn't panic, the event listener registration works
328    }
329
330    #[test]
331    fn test_builder_errors_without_key_extractor() {
332        let result = CacheLayer::<TestRequest, String>::builder().build();
333        assert!(matches!(
334            result,
335            Err(crate::CacheBuildError::MissingKeyExtractor)
336        ));
337    }
338}