priority_lfu/lifecycle.rs
1//! Lifecycle hooks for cache items.
2//!
3//! The [`Lifecycle`] trait allows you to hook into cache item lifecycle events,
4//! such as eviction. This is useful for maintaining external indexes or performing
5//! cleanup when items are removed from the cache.
6//!
7//! # Example
8//!
9//! ```
10//! use std::any::Any;
11//! use priority_lfu::{Lifecycle, CacheBuilder, CacheKey, DeepSizeOf};
12//!
13//! #[derive(Hash, Eq, PartialEq, Clone)]
14//! struct MyKey(u64);
15//!
16//! #[derive(Clone, DeepSizeOf)]
17//! struct MyValue(String);
18//!
19//! impl CacheKey for MyKey {
20//! type Value = MyValue;
21//! }
22//!
23//! // Lifecycle that tracks evicted keys
24//! #[derive(Clone)]
25//! struct MyLifecycle;
26//!
27//! impl Lifecycle for MyLifecycle {
28//! fn on_evict(&self, key: &dyn Any) {
29//! if let Some(key) = key.downcast_ref::<MyKey>() {
30//! println!("Evicted key: {}", key.0);
31//! }
32//! }
33//! }
34//!
35//! let cache = CacheBuilder::new(1024)
36//! .lifecycle(MyLifecycle)
37//! .build();
38//! ```
39
40use std::any::Any;
41use std::sync::Arc;
42
43/// Hooks into the lifetime of cache items.
44///
45/// The functions should be small and very fast, otherwise the cache performance
46/// might be negatively affected. Lifecycle methods are called synchronously after
47/// releasing the shard lock.
48///
49/// # Type Erasure
50///
51/// Because the cache supports heterogeneous key types via type erasure, the lifecycle
52/// methods receive type-erased `&dyn Any` references. Use `downcast_ref` to recover
53/// the concrete types:
54///
55/// ```ignore
56/// fn on_evict(&self, key: &dyn Any) {
57/// if let Some(key) = key.downcast_ref::<MyKey>() {
58/// // Handle eviction of MyKey
59/// }
60/// }
61/// ```
62pub trait Lifecycle: Send + Sync {
63 /// Called when an item is evicted from the cache due to capacity pressure.
64 ///
65 /// This is called during eviction triggered by insert operations that need space.
66 ///
67 /// # Arguments
68 ///
69 /// * `key` - Reference to the evicted key (downcast to your key type)
70 fn on_evict(&self, key: &dyn Any);
71
72 /// Called when an item is explicitly removed via `cache.remove()`.
73 ///
74 /// By default, this calls [`on_evict`]. Override if you need different behavior
75 /// for explicit removals vs automatic evictions.
76 ///
77 /// # Arguments
78 ///
79 /// * `key` - Reference to the removed key (downcast to your key type)
80 fn on_remove(&self, key: &dyn Any) {
81 self.on_evict(key);
82 }
83
84 /// Called when the cache is cleared via `cache.clear()`.
85 ///
86 /// By default, this calls [`on_evict`] for each entry. Override if you need
87 /// bulk cleanup logic instead of per-entry callbacks.
88 ///
89 /// # Arguments
90 ///
91 /// * `key` - Reference to the cleared key (downcast to your key type)
92 fn on_clear(&self, key: &dyn Any) {
93 self.on_evict(key);
94 }
95}
96
97/// Default lifecycle implementation that does nothing.
98///
99/// This is used when no lifecycle hooks are needed.
100#[derive(Debug, Clone, Copy, Default)]
101pub struct DefaultLifecycle;
102
103impl Lifecycle for DefaultLifecycle {
104 #[inline]
105 fn on_evict(&self, _key: &dyn Any) {
106 // No-op
107 }
108
109 #[inline]
110 fn on_remove(&self, _key: &dyn Any) {
111 // No-op
112 }
113
114 #[inline]
115 fn on_clear(&self, _key: &dyn Any) {
116 // No-op
117 }
118}
119
120/// Lifecycle implementation that wraps an `Arc<L>` for shared ownership.
121///
122/// This is used internally when the cache needs to clone the lifecycle.
123impl<L: Lifecycle> Lifecycle for Arc<L> {
124 #[inline]
125 fn on_evict(&self, key: &dyn Any) {
126 (**self).on_evict(key);
127 }
128
129 #[inline]
130 fn on_remove(&self, key: &dyn Any) {
131 (**self).on_remove(key);
132 }
133
134 #[inline]
135 fn on_clear(&self, key: &dyn Any) {
136 (**self).on_clear(key);
137 }
138}
139
140/// Helper for creating typed lifecycle implementations.
141///
142/// This wrapper provides convenient typed access to lifecycle events when you only
143/// use a single key type in your cache.
144///
145/// # Example
146///
147/// ```
148/// use priority_lfu::{TypedLifecycle, CacheBuilder, CacheKey, DeepSizeOf};
149/// use std::sync::atomic::{AtomicUsize, Ordering};
150/// use std::sync::Arc;
151///
152/// #[derive(Hash, Eq, PartialEq, Clone)]
153/// struct MyKey(u64);
154///
155/// #[derive(Clone, DeepSizeOf)]
156/// struct MyValue(String);
157///
158/// impl CacheKey for MyKey {
159/// type Value = MyValue;
160/// }
161///
162/// let evict_count = Arc::new(AtomicUsize::new(0));
163/// let counter = evict_count.clone();
164///
165/// let lifecycle = TypedLifecycle::<MyKey, _>::new(move |_key| {
166/// counter.fetch_add(1, Ordering::Relaxed);
167/// });
168///
169/// let cache = CacheBuilder::new(1024)
170/// .lifecycle(lifecycle)
171/// .build();
172/// ```
173pub struct TypedLifecycle<K, F>
174where
175 F: Fn(K) + Send + Sync,
176{
177 callback: F,
178 _marker: std::marker::PhantomData<fn(K)>,
179}
180
181impl<K, F> TypedLifecycle<K, F>
182where
183 F: Fn(K) + Send + Sync,
184{
185 /// Create a new typed lifecycle with the given eviction callback.
186 pub fn new(callback: F) -> Self {
187 Self {
188 callback,
189 _marker: std::marker::PhantomData,
190 }
191 }
192}
193
194impl<K, F> Lifecycle for TypedLifecycle<K, F>
195where
196 K: Clone + Send + Sync + 'static,
197 F: Fn(K) + Send + Sync,
198{
199 fn on_evict(&self, key: &dyn Any) {
200 // Try to downcast key to the expected type
201 if let Some(key) = key.downcast_ref::<K>() {
202 (self.callback)(key.clone());
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use std::sync::atomic::{AtomicUsize, Ordering};
210
211 use super::*;
212
213 #[test]
214 fn test_default_lifecycle() {
215 let lifecycle = DefaultLifecycle;
216
217 // Should not panic
218 lifecycle.on_evict(&42u64);
219 lifecycle.on_remove(&42u64);
220 lifecycle.on_clear(&42u64);
221 }
222
223 #[test]
224 fn test_typed_lifecycle() {
225 let counter = Arc::new(AtomicUsize::new(0));
226 let counter_clone = counter.clone();
227
228 let lifecycle = TypedLifecycle::<u64, _>::new(move |key| {
229 assert_eq!(key, 42);
230 counter_clone.fetch_add(1, Ordering::Relaxed);
231 });
232
233 // Matching types should call the callback
234 lifecycle.on_evict(&42u64);
235 assert_eq!(counter.load(Ordering::Relaxed), 1);
236
237 // Non-matching key type should not call the callback
238 lifecycle.on_evict(&"wrong_type");
239 assert_eq!(counter.load(Ordering::Relaxed), 1);
240 }
241
242 #[test]
243 fn test_arc_lifecycle() {
244 let counter = Arc::new(AtomicUsize::new(0));
245 let counter_clone = counter.clone();
246
247 let lifecycle = TypedLifecycle::<u64, _>::new(move |_| {
248 counter_clone.fetch_add(1, Ordering::Relaxed);
249 });
250
251 let arc_lifecycle = Arc::new(lifecycle);
252
253 arc_lifecycle.on_evict(&42u64);
254 assert_eq!(counter.load(Ordering::Relaxed), 1);
255 }
256}