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}