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}