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    /// ```
95    ///
96    /// Default: `EvictionPolicy::Lru`
97    pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
98        self.eviction_policy = policy;
99        self
100    }
101
102    /// Sets the function that extracts a cache key from a request.
103    ///
104    /// This function must be provided before building.
105    pub fn key_extractor<F>(mut self, f: F) -> Self
106    where
107        F: Fn(&Req) -> K + Send + Sync + 'static,
108    {
109        self.key_extractor = Some(Arc::new(f));
110        self
111    }
112
113    /// Sets the name of this cache instance for observability.
114    ///
115    /// Default: `"<unnamed>"`
116    pub fn name(mut self, name: impl Into<String>) -> Self {
117        self.name = name.into();
118        self
119    }
120
121    /// Registers a callback when a cache hit occurs.
122    ///
123    /// A cache hit occurs when a requested entry is found in the cache and has not expired.
124    ///
125    /// # Callback Signature
126    /// `Fn()` - Called with no parameters when a cache hit is detected.
127    ///
128    /// # Example
129    /// ```rust,no_run
130    /// use tower_resilience_cache::CacheLayer;
131    /// use std::sync::atomic::{AtomicUsize, Ordering};
132    /// use std::sync::Arc;
133    ///
134    /// #[derive(Clone, Hash, Eq, PartialEq)]
135    /// struct Request {
136    ///     id: String,
137    /// }
138    ///
139    /// let hit_count = Arc::new(AtomicUsize::new(0));
140    /// let counter = Arc::clone(&hit_count);
141    ///
142    /// let config = CacheLayer::<Request, String>::builder()
143    ///     .key_extractor(|req| req.id.clone())
144    ///     .on_hit(move || {
145    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
146    ///         println!("Cache hit #{}", count + 1);
147    ///     })
148    ///     .build();
149    /// ```
150    pub fn on_hit<F>(mut self, f: F) -> Self
151    where
152        F: Fn() + Send + Sync + 'static,
153    {
154        self.event_listeners.add(FnListener::new(move |event| {
155            if matches!(event, CacheEvent::Hit { .. }) {
156                f();
157            }
158        }));
159        self
160    }
161
162    /// Registers a callback when a cache miss occurs.
163    ///
164    /// A cache miss occurs when a requested entry is not found in the cache or has expired.
165    /// The underlying service will be called to fetch the value, which will then be cached.
166    ///
167    /// # Callback Signature
168    /// `Fn()` - Called with no parameters when a cache miss is detected.
169    ///
170    /// # Example
171    /// ```rust,no_run
172    /// use tower_resilience_cache::CacheLayer;
173    /// use std::sync::atomic::{AtomicUsize, Ordering};
174    /// use std::sync::Arc;
175    ///
176    /// #[derive(Clone, Hash, Eq, PartialEq)]
177    /// struct Request {
178    ///     id: String,
179    /// }
180    ///
181    /// let miss_count = Arc::new(AtomicUsize::new(0));
182    /// let counter = Arc::clone(&miss_count);
183    ///
184    /// let config = CacheLayer::<Request, String>::builder()
185    ///     .key_extractor(|req| req.id.clone())
186    ///     .on_miss(move || {
187    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
188    ///         println!("Cache miss #{} - fetching from service", count + 1);
189    ///     })
190    ///     .build();
191    /// ```
192    pub fn on_miss<F>(mut self, f: F) -> Self
193    where
194        F: Fn() + Send + Sync + 'static,
195    {
196        self.event_listeners.add(FnListener::new(move |event| {
197            if matches!(event, CacheEvent::Miss { .. }) {
198                f();
199            }
200        }));
201        self
202    }
203
204    /// Registers a callback when an entry is evicted from the cache.
205    ///
206    /// Eviction occurs when:
207    /// - The cache reaches its maximum size and needs to make room for new entries
208    /// - An entry expires due to TTL (time-to-live) configuration
209    ///
210    /// # Callback Signature
211    /// `Fn()` - Called with no parameters when a cache eviction occurs.
212    ///
213    /// # Example
214    /// ```rust,no_run
215    /// use tower_resilience_cache::CacheLayer;
216    /// use std::sync::atomic::{AtomicUsize, Ordering};
217    /// use std::sync::Arc;
218    /// use std::time::Duration;
219    ///
220    /// #[derive(Clone, Hash, Eq, PartialEq)]
221    /// struct Request {
222    ///     id: String,
223    /// }
224    ///
225    /// let eviction_count = Arc::new(AtomicUsize::new(0));
226    /// let counter = Arc::clone(&eviction_count);
227    ///
228    /// let config = CacheLayer::<Request, String>::builder()
229    ///     .key_extractor(|req| req.id.clone())
230    ///     .max_size(100)
231    ///     .ttl(Duration::from_secs(300))
232    ///     .on_eviction(move || {
233    ///         let count = counter.fetch_add(1, Ordering::SeqCst);
234    ///         println!("Entry evicted (total: {})", count + 1);
235    ///     })
236    ///     .build();
237    /// ```
238    pub fn on_eviction<F>(mut self, f: F) -> Self
239    where
240        F: Fn() + Send + Sync + 'static,
241    {
242        self.event_listeners.add(FnListener::new(move |event| {
243            if matches!(event, CacheEvent::Eviction { .. }) {
244                f();
245            }
246        }));
247        self
248    }
249
250    /// Builds the cache layer.
251    ///
252    /// # Panics
253    ///
254    /// Panics if `key_extractor` was not set.
255    pub fn build(self) -> crate::CacheLayer<Req, K> {
256        let key_extractor = self
257            .key_extractor
258            .expect("key_extractor must be set before building");
259
260        let config = CacheConfig {
261            max_size: self.max_size,
262            ttl: self.ttl,
263            eviction_policy: self.eviction_policy,
264            key_extractor,
265            event_listeners: self.event_listeners,
266            name: self.name,
267        };
268
269        crate::CacheLayer::new(config)
270    }
271}
272
273impl<Req, K> Default for CacheConfigBuilder<Req, K>
274where
275    K: Hash + Eq + Clone + Send + 'static,
276{
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::CacheLayer;
286
287    #[derive(Clone, Hash, Eq, PartialEq)]
288    struct TestRequest {
289        id: String,
290    }
291
292    #[test]
293    fn test_builder_defaults() {
294        let _layer = CacheLayer::<TestRequest, String>::builder()
295            .key_extractor(|req| req.id.clone())
296            .build();
297        // If this compiles and doesn't panic, the builder works
298    }
299
300    #[test]
301    fn test_builder_custom_values() {
302        let _layer = CacheLayer::<TestRequest, String>::builder()
303            .max_size(500)
304            .ttl(Duration::from_secs(60))
305            .key_extractor(|req| req.id.clone())
306            .name("my-cache")
307            .build();
308        // If this compiles and doesn't panic, the builder works
309    }
310
311    #[test]
312    fn test_event_listeners() {
313        let _layer = CacheLayer::<TestRequest, String>::builder()
314            .key_extractor(|req| req.id.clone())
315            .on_hit(|| {})
316            .on_miss(|| {})
317            .on_eviction(|| {})
318            .build();
319        // If this compiles and doesn't panic, the event listener registration works
320    }
321
322    #[test]
323    #[should_panic(expected = "key_extractor must be set")]
324    fn test_builder_panics_without_key_extractor() {
325        let _config = CacheLayer::<TestRequest, String>::builder().build();
326    }
327}