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