Skip to main content

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}