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}