diff --git a/Cargo.lock b/Cargo.lock index 2560e13..257a2bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,7 @@ dependencies = [ "proptest", "quick_cache", "rustc-hash 2.1.1", + "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 7f9efbb..fdf4a21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ crate-type = ["lib"] [features] default = ["policy-s3-fifo", "policy-lru", "policy-fast-lru", "policy-lru-k", "policy-clock"] metrics = [] +serde = ["dep:serde"] concurrency = ["parking_lot"] # Eviction policy feature flags. Enable only the policies you need for smaller builds. @@ -81,6 +82,7 @@ policy-nru = [] [dependencies] parking_lot = { version = "0.12", optional = true } rustc-hash = "2.1" +serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] bench-support = { path = "bench-support" } diff --git a/src/store/handle.rs b/src/store/handle.rs index de61be7..efc1762 100644 --- a/src/store/handle.rs +++ b/src/store/handle.rs @@ -93,27 +93,24 @@ //! ## Example Usage //! //! ```rust -//! use std::sync::Arc; -//! use cachekit::ds::KeyInterner; -//! use cachekit::store::handle::HandleStore; -//! -//! // Create interner and store +//! # use std::sync::Arc; +//! # use cachekit::ds::KeyInterner; +//! # use cachekit::store::handle::HandleStore; +//! # use cachekit::store::traits::StoreFull; +//! # fn main() -> Result<(), StoreFull> { //! let mut interner = KeyInterner::new(); //! let mut store: HandleStore = HandleStore::new(100); //! -//! // Intern a key to get a handle //! let handle = interner.intern(&"user:12345".to_string()); +//! store.try_insert(handle, Arc::new("cached_data".to_string()))?; //! -//! // Store value by handle -//! store.try_insert(handle, Arc::new("cached_data".to_string())).unwrap(); -//! -//! // Retrieve by handle //! assert_eq!(store.get(&handle), Some(Arc::new("cached_data".to_string()))); //! -//! // Check metrics //! let metrics = store.metrics(); //! assert_eq!(metrics.hits, 1); //! assert_eq!(metrics.inserts, 1); +//! # Ok(()) +//! # } //! ``` //! //! ## Type Constraints @@ -152,7 +149,7 @@ use crate::store::traits::{StoreFull, StoreMetrics}; /// /// Uses `Cell` for interior mutability without synchronization overhead. /// Not thread-safe—intended for use with [`HandleStore`] only. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] struct StoreCounters { /// Successful lookups via `get()`. hits: Cell, @@ -282,35 +279,32 @@ impl ConcurrentStoreCounters { /// # Example /// /// ``` -/// use std::sync::Arc; -/// use cachekit::store::handle::HandleStore; -/// +/// # use std::sync::Arc; +/// # use cachekit::store::handle::HandleStore; +/// # use cachekit::store::traits::StoreFull; +/// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(3); /// -/// // Insert entries using handles (e.g., from an interner) /// let handle_a = 1u64; /// let handle_b = 2u64; /// -/// store.try_insert(handle_a, Arc::new("alice".into())).unwrap(); -/// store.try_insert(handle_b, Arc::new("bob".into())).unwrap(); +/// store.try_insert(handle_a, Arc::new("alice".into()))?; +/// store.try_insert(handle_b, Arc::new("bob".into()))?; /// -/// // Lookup returns Arc /// assert_eq!(store.get(&handle_a), Some(Arc::new("alice".into()))); -/// -/// // Peek without affecting metrics /// assert!(store.peek(&handle_b).is_some()); /// -/// // Update existing entry -/// let old = store.try_insert(handle_a, Arc::new("alice_v2".into())).unwrap(); +/// let old = store.try_insert(handle_a, Arc::new("alice_v2".into()))?; /// assert_eq!(old, Some(Arc::new("alice".into()))); /// -/// // Check metrics /// let m = store.metrics(); -/// assert_eq!(m.inserts, 2); // handle_a, handle_b -/// assert_eq!(m.updates, 1); // handle_a updated -/// assert_eq!(m.hits, 1); // one get() call +/// assert_eq!(m.inserts, 2); +/// assert_eq!(m.updates, 1); +/// assert_eq!(m.hits, 1); +/// # Ok(()) +/// # } /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct HandleStore { map: FxHashMap>, capacity: usize, @@ -347,18 +341,21 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::HandleStore; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::HandleStore; + /// # use cachekit::store::traits::StoreFull; + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(10); - /// store.try_insert(1, Arc::new("value")).unwrap(); + /// store.try_insert(1, Arc::new("value"))?; /// /// assert_eq!(store.get(&1), Some(Arc::new("value"))); - /// assert_eq!(store.get(&999), None); // miss + /// assert_eq!(store.get(&999), None); /// /// let m = store.metrics(); /// assert_eq!(m.hits, 1); /// assert_eq!(m.misses, 1); + /// # Ok(()) + /// # } /// ``` pub fn get(&self, handle: &H) -> Option> { match self.map.get(handle).cloned() { @@ -380,15 +377,17 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::HandleStore; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::HandleStore; + /// # use cachekit::store::traits::StoreFull; + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(10); - /// store.try_insert(1, Arc::new(42)).unwrap(); + /// store.try_insert(1, Arc::new(42))?; /// - /// // Peek doesn't update metrics /// assert_eq!(store.peek(&1), Some(&Arc::new(42))); /// assert_eq!(store.metrics().hits, 0); + /// # Ok(()) + /// # } /// ``` pub fn peek(&self, handle: &H) -> Option<&Arc> { self.map.get(handle) @@ -449,15 +448,18 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::HandleStore; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::HandleStore; + /// # use cachekit::store::traits::StoreFull; + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(10); - /// store.try_insert(1, Arc::new("value")).unwrap(); + /// store.try_insert(1, Arc::new("value"))?; /// /// assert_eq!(store.remove(&1), Some(Arc::new("value"))); - /// assert_eq!(store.remove(&1), None); // already removed + /// assert_eq!(store.remove(&1), None); /// assert_eq!(store.metrics().removes, 1); + /// # Ok(()) + /// # } /// ``` pub fn remove(&mut self, handle: &H) -> Option> { let removed = self.map.remove(handle); @@ -494,18 +496,21 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::HandleStore; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::HandleStore; + /// # use cachekit::store::traits::StoreFull; + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(10); - /// store.try_insert(1, Arc::new(100)).unwrap(); + /// store.try_insert(1, Arc::new(100))?; /// store.get(&1); - /// store.get(&999); // miss + /// store.get(&999); /// /// let m = store.metrics(); /// assert_eq!(m.inserts, 1); /// assert_eq!(m.hits, 1); /// assert_eq!(m.misses, 1); + /// # Ok(()) + /// # } /// ``` pub fn metrics(&self) -> StoreMetrics { self.metrics.snapshot() @@ -519,19 +524,21 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::HandleStore; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::HandleStore; + /// # use cachekit::store::traits::StoreFull; + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HandleStore = HandleStore::new(10); - /// store.try_insert(1, Arc::new(100)).unwrap(); + /// store.try_insert(1, Arc::new(100))?; /// - /// // Policy decides to evict /// store.remove(&1); /// store.record_eviction(); /// /// let m = store.metrics(); /// assert_eq!(m.removes, 1); /// assert_eq!(m.evictions, 1); + /// # Ok(()) + /// # } /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); @@ -562,34 +569,31 @@ where /// # Example /// /// ``` -/// use std::sync::Arc; -/// use std::thread; -/// use cachekit::store::handle::ConcurrentHandleStore; -/// use cachekit::store::traits::ConcurrentStoreRead; -/// +/// # use std::sync::Arc; +/// # use std::thread; +/// # use cachekit::store::handle::ConcurrentHandleStore; +/// # use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead, StoreFull}; /// let store = Arc::new(ConcurrentHandleStore::::new(100)); /// -/// // Spawn writers /// let store_w = Arc::clone(&store); -/// let writer = thread::spawn(move || { -/// use cachekit::store::traits::ConcurrentStore; +/// let writer = thread::spawn(move || -> Result<(), StoreFull> { /// for i in 0..10 { -/// store_w.try_insert(i, Arc::new(format!("value_{}", i))).unwrap(); +/// store_w.try_insert(i, Arc::new(format!("value_{}", i)))?; /// } +/// Ok(()) /// }); /// -/// // Spawn readers /// let store_r = Arc::clone(&store); /// let reader = thread::spawn(move || { -/// // Readers may see partial writes /// let _ = store_r.get(&5); /// store_r.len() /// }); /// -/// writer.join().unwrap(); -/// reader.join().unwrap(); +/// writer.join().expect("writer panicked")?; +/// reader.join().expect("reader panicked"); /// /// assert_eq!(store.len(), 10); +/// # Ok::<(), StoreFull>(()) /// ``` /// /// # Trait Implementations @@ -640,18 +644,19 @@ where /// # Example /// /// ``` - /// use std::sync::Arc; - /// use cachekit::store::handle::ConcurrentHandleStore; - /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead}; - /// + /// # use std::sync::Arc; + /// # use cachekit::store::handle::ConcurrentHandleStore; + /// # use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead, StoreFull}; + /// # fn main() -> Result<(), StoreFull> { /// let store: ConcurrentHandleStore = ConcurrentHandleStore::new(10); - /// store.try_insert(1, Arc::new(100)).unwrap(); + /// store.try_insert(1, Arc::new(100))?; /// - /// // Policy evicts the entry /// store.remove(&1); /// store.record_eviction(); /// /// assert_eq!(store.metrics().evictions, 1); + /// # Ok(()) + /// # } /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); diff --git a/src/store/hashmap.rs b/src/store/hashmap.rs index 611bd4e..49423a2 100644 --- a/src/store/hashmap.rs +++ b/src/store/hashmap.rs @@ -116,12 +116,13 @@ //! //! ```rust //! use cachekit::store::hashmap::HashMapStore; -//! use cachekit::store::traits::{StoreCore, StoreMut}; +//! use cachekit::store::traits::{StoreCore, StoreFull, StoreMut}; //! +//! # fn main() -> Result<(), StoreFull> { //! let mut store: HashMapStore<&str, i32> = HashMapStore::new(100); //! //! // Insert and access -//! store.try_insert("key", 42).unwrap(); +//! store.try_insert("key", 42)?; //! assert_eq!(store.get(&"key"), Some(&42)); // Returns &V, zero-copy //! //! // Peek without metrics @@ -131,6 +132,8 @@ //! let m = store.metrics(); //! assert_eq!(m.hits, 1); // get() counted //! assert_eq!(m.inserts, 1); +//! # Ok(()) +//! # } //! ``` //! //! ## Type Constraints @@ -248,12 +251,13 @@ impl StoreCounters { /// /// ``` /// use cachekit::store::hashmap::HashMapStore; -/// use cachekit::store::traits::{StoreCore, StoreMut}; +/// use cachekit::store::traits::{StoreCore, StoreFull, StoreMut}; /// +/// # fn main() -> Result<(), StoreFull> { /// let mut store: HashMapStore> = HashMapStore::new(1000); /// /// // Insert data -/// store.try_insert("image.png".into(), vec![0x89, 0x50, 0x4E, 0x47]).unwrap(); +/// store.try_insert("image.png".into(), vec![0x89, 0x50, 0x4E, 0x47])?; /// /// // Access returns &V (zero-copy) /// let data: &Vec = store.get(&"image.png".into()).unwrap(); @@ -268,6 +272,8 @@ impl StoreCounters { /// let m = store.metrics(); /// assert_eq!(m.hits, 1); /// assert_eq!(m.inserts, 1); +/// # Ok(()) +/// # } /// ``` /// /// # Custom Hasher @@ -348,14 +354,17 @@ where /// /// ``` /// use cachekit::store::hashmap::HashMapStore; - /// use cachekit::store::traits::{StoreCore, StoreMut}; + /// use cachekit::store::traits::{StoreCore, StoreFull, StoreMut}; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HashMapStore<&str, i32> = HashMapStore::new(10); - /// store.try_insert("key", 42).unwrap(); + /// store.try_insert("key", 42)?; /// /// // Peek doesn't update metrics /// assert_eq!(store.peek(&"key"), Some(&42)); /// assert_eq!(store.metrics().hits, 0); + /// # Ok(()) + /// # } /// ``` pub fn peek(&self, key: &K) -> Option<&V> { self.map.get(key) @@ -369,10 +378,11 @@ where /// /// ``` /// use cachekit::store::hashmap::HashMapStore; - /// use cachekit::store::traits::StoreMut; + /// use cachekit::store::traits::{StoreFull, StoreMut}; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HashMapStore<&str, Vec> = HashMapStore::new(10); - /// store.try_insert("nums", vec![1, 2, 3]).unwrap(); + /// store.try_insert("nums", vec![1, 2, 3])?; /// /// // Modify in place /// if let Some(nums) = store.peek_mut(&"nums") { @@ -380,6 +390,8 @@ where /// } /// /// assert_eq!(store.peek(&"nums"), Some(&vec![1, 2, 3, 4])); + /// # Ok(()) + /// # } /// ``` pub fn peek_mut(&mut self, key: &K) -> Option<&mut V> { self.map.get_mut(key) @@ -388,6 +400,16 @@ where /// Returns the underlying HashMap's allocated capacity. /// /// This may be larger than the logical capacity limit set at creation. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::hashmap::HashMapStore; + /// use cachekit::store::traits::StoreCore; + /// + /// let store: HashMapStore<&str, i32> = HashMapStore::new(100); + /// assert!(store.map_capacity() >= store.capacity()); + /// ``` pub fn map_capacity(&self) -> usize { self.map.capacity() } @@ -401,10 +423,11 @@ where /// /// ``` /// use cachekit::store::hashmap::HashMapStore; - /// use cachekit::store::traits::{StoreCore, StoreMut}; + /// use cachekit::store::traits::{StoreCore, StoreFull, StoreMut}; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store: HashMapStore<&str, i32> = HashMapStore::new(10); - /// store.try_insert("key", 1).unwrap(); + /// store.try_insert("key", 1)?; /// /// // Policy evicts the entry /// store.remove(&"key"); @@ -413,12 +436,31 @@ where /// let m = store.metrics(); /// assert_eq!(m.removes, 1); /// assert_eq!(m.evictions, 1); + /// # Ok(()) + /// # } /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } } +impl Clone for HashMapStore { + fn clone(&self) -> Self { + Self { + map: self.map.clone(), + capacity: self.capacity, + metrics: StoreCounters { + hits: AtomicU64::new(self.metrics.hits.load(Ordering::Relaxed)), + misses: AtomicU64::new(self.metrics.misses.load(Ordering::Relaxed)), + inserts: AtomicU64::new(self.metrics.inserts.load(Ordering::Relaxed)), + updates: AtomicU64::new(self.metrics.updates.load(Ordering::Relaxed)), + removes: AtomicU64::new(self.metrics.removes.load(Ordering::Relaxed)), + evictions: AtomicU64::new(self.metrics.evictions.load(Ordering::Relaxed)), + }, + } + } +} + /// Read operations for [`HashMapStore`]. /// /// Returns borrowed `&V` references for zero-copy access. @@ -646,6 +688,23 @@ where /// Records an eviction in the metrics. /// /// Thread-safe via atomic increment. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::hashmap::ConcurrentHashMapStore; + /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead, StoreFull}; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let store = ConcurrentHashMapStore::<&str, i32>::new(10); + /// store.try_insert("key", Arc::new(1))?; + /// store.remove(&"key"); + /// store.record_eviction(); + /// assert_eq!(store.metrics().evictions, 1); + /// # Ok(()) + /// # } + /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } @@ -935,6 +994,23 @@ where /// Records an eviction in the metrics. /// /// Thread-safe via atomic increment. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::hashmap::ShardedHashMapStore; + /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead, StoreFull}; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let store = ShardedHashMapStore::<&str, i32>::new(10, 4); + /// store.try_insert("key", Arc::new(1))?; + /// store.remove(&"key"); + /// store.record_eviction(); + /// assert_eq!(store.metrics().evictions, 1); + /// # Ok(()) + /// # } + /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } diff --git a/src/store/slab.rs b/src/store/slab.rs index c09b953..421992b 100644 --- a/src/store/slab.rs +++ b/src/store/slab.rs @@ -119,10 +119,11 @@ //! use cachekit::store::slab::{SlabStore, EntryId}; //! use cachekit::store::traits::{StoreCore, StoreMut}; //! +//! # fn main() -> Result<(), cachekit::store::traits::StoreFull> { //! let mut store: SlabStore> = SlabStore::new(100); //! //! // Insert and get stable handle -//! store.try_insert("image.png".into(), vec![0x89, 0x50]).unwrap(); +//! store.try_insert("image.png".into(), vec![0x89, 0x50])?; //! let id: EntryId = store.entry_id(&"image.png".into()).unwrap(); //! //! // Access by handle (O(1), no hash lookup) @@ -133,9 +134,11 @@ //! //! // Remove and verify slot reuse //! store.remove(&"image.png".into()); -//! store.try_insert("icon.png".into(), vec![0x00]).unwrap(); +//! store.try_insert("icon.png".into(), vec![0x00])?; //! let new_id = store.entry_id(&"icon.png".into()).unwrap(); //! assert_eq!(id.index(), new_id.index()); // Same slot reused +//! # Ok(()) +//! # } //! ``` //! //! ## Type Constraints @@ -191,8 +194,9 @@ use crate::store::traits::{StoreCore, StoreFactory, StoreFull, StoreMetrics, Sto /// use cachekit::store::slab::{SlabStore, EntryId}; /// use cachekit::store::traits::StoreMut; /// +/// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); -/// store.try_insert("key", 42).unwrap(); +/// store.try_insert("key", 42)?; /// /// // Get stable handle /// let id: EntryId = store.entry_id(&"key").unwrap(); @@ -202,8 +206,11 @@ use crate::store::traits::{StoreCore, StoreFactory, StoreFull, StoreMetrics, Sto /// /// // Inspect raw index (for debugging/logging) /// println!("Entry at slot {}", id.index()); +/// # Ok(()) +/// # } /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[must_use] pub struct EntryId(usize); impl EntryId { @@ -211,6 +218,7 @@ impl EntryId { /// /// Useful for debugging, logging, or custom data structures that need /// to track slot positions. + #[must_use] pub fn index(&self) -> usize { self.0 } @@ -300,11 +308,12 @@ impl StoreCounters { /// use cachekit::store::slab::{SlabStore, EntryId}; /// use cachekit::store::traits::{StoreCore, StoreMut}; /// +/// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore> = SlabStore::new(100); /// /// // Insert entries -/// store.try_insert("file1.txt".into(), vec![1, 2, 3]).unwrap(); -/// store.try_insert("file2.txt".into(), vec![4, 5, 6]).unwrap(); +/// store.try_insert("file1.txt".into(), vec![1, 2, 3])?; +/// store.try_insert("file2.txt".into(), vec![4, 5, 6])?; /// /// // Get stable handle for policy metadata /// let id1 = store.entry_id(&"file1.txt".into()).unwrap(); @@ -319,9 +328,11 @@ impl StoreCounters { /// /// // Remove and observe slot reuse /// store.remove(&"file1.txt".into()); -/// store.try_insert("file3.txt".into(), vec![7, 8, 9]).unwrap(); +/// store.try_insert("file3.txt".into(), vec![7, 8, 9])?; /// let id3 = store.entry_id(&"file3.txt".into()).unwrap(); /// assert_eq!(id1.index(), id3.index()); // Same slot reused +/// # Ok(()) +/// # } /// ``` /// /// # Policy Integration @@ -330,8 +341,9 @@ impl StoreCounters { /// use cachekit::store::slab::{SlabStore, EntryId}; /// use cachekit::store::traits::StoreMut; /// +/// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore = SlabStore::new(10); -/// store.try_insert(1, "value".into()).unwrap(); +/// store.try_insert(1, "value".into())?; /// /// // Policy tracks EntryId for O(1) eviction /// let victim_id = store.entry_id(&1).unwrap(); @@ -342,8 +354,11 @@ impl StoreCounters { /// /// assert_eq!(key, 1); /// assert_eq!(value, "value"); +/// # Ok(()) +/// # } /// ``` #[derive(Debug)] +#[must_use] pub struct SlabStore { entries: Vec>>, free_list: Vec, @@ -390,13 +405,16 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::StoreMut; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); - /// store.try_insert("key", 42).unwrap(); + /// store.try_insert("key", 42)?; /// /// let id = store.entry_id(&"key").unwrap(); /// assert_eq!(store.get_by_id(id), Some(&42)); /// /// assert!(store.entry_id(&"missing").is_none()); + /// # Ok(()) + /// # } /// ``` pub fn entry_id(&self, key: &K) -> Option { self.index.get(key).copied() @@ -412,11 +430,14 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::StoreMut; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); - /// store.try_insert("key", 100).unwrap(); + /// store.try_insert("key", 100)?; /// let id = store.entry_id(&"key").unwrap(); /// /// assert_eq!(store.get_by_id(id), Some(&100)); + /// # Ok(()) + /// # } /// ``` pub fn get_by_id(&self, id: EntryId) -> Option<&V> { self.entries @@ -434,8 +455,9 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::StoreMut; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, Vec> = SlabStore::new(10); - /// store.try_insert("nums", vec![1, 2, 3]).unwrap(); + /// store.try_insert("nums", vec![1, 2, 3])?; /// let id = store.entry_id(&"nums").unwrap(); /// /// // Modify in place @@ -444,6 +466,8 @@ where /// } /// /// assert_eq!(store.get_by_id(id), Some(&vec![1, 2, 3, 4])); + /// # Ok(()) + /// # } /// ``` pub fn get_by_id_mut(&mut self, id: EntryId) -> Option<&mut V> { self.entries @@ -461,11 +485,14 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::StoreMut; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore = SlabStore::new(10); - /// store.try_insert("my_key".into(), 42).unwrap(); + /// store.try_insert("my_key".into(), 42)?; /// let id = store.entry_id(&"my_key".into()).unwrap(); /// /// assert_eq!(store.key_by_id(id), Some(&"my_key".into())); + /// # Ok(()) + /// # } /// ``` pub fn key_by_id(&self, id: EntryId) -> Option<&K> { self.entries @@ -481,12 +508,15 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::{StoreCore, StoreMut}; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); - /// store.try_insert("key", 42).unwrap(); + /// store.try_insert("key", 42)?; /// /// // Peek doesn't update metrics /// assert_eq!(store.peek(&"key"), Some(&42)); /// assert_eq!(store.metrics().hits, 0); + /// # Ok(()) + /// # } /// ``` pub fn peek(&self, key: &K) -> Option<&V> { self.index.get(key).and_then(|id| self.get_by_id(*id)) @@ -513,8 +543,9 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::{StoreCore, StoreMut}; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); - /// store.try_insert("victim", 1).unwrap(); + /// store.try_insert("victim", 1)?; /// let id = store.entry_id(&"victim").unwrap(); /// /// // Policy evicts @@ -524,6 +555,8 @@ where /// let m = store.metrics(); /// assert_eq!(m.removes, 1); /// assert_eq!(m.evictions, 1); + /// # Ok(()) + /// # } /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); @@ -540,8 +573,9 @@ where /// use cachekit::store::slab::SlabStore; /// use cachekit::store::traits::{StoreCore, StoreMut}; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let mut store: SlabStore<&str, i32> = SlabStore::new(10); - /// store.try_insert("key", 42).unwrap(); + /// store.try_insert("key", 42)?; /// let id = store.entry_id(&"key").unwrap(); /// /// let (key, value) = store.remove_by_id(id).unwrap(); @@ -550,6 +584,8 @@ where /// /// // Slot is now free for reuse /// assert!(!store.contains(&"key")); + /// # Ok(()) + /// # } /// ``` pub fn remove_by_id(&mut self, id: EntryId) -> Option<(K, V)> { let entry = self.entries.get_mut(id.0)?.take()?; @@ -621,6 +657,11 @@ where /// # Errors /// /// Returns [`StoreFull`] if at capacity and the key is new. + /// + /// # Panics + /// + /// Panics if the index references a slot that is unexpectedly empty, + /// indicating an internal invariant violation. fn try_insert(&mut self, key: K, value: V) -> Result, StoreFull> { if let Some(id) = self.index.get(&key).copied() { let entry = self.entries[id.0].as_mut().expect("slab entry missing"); @@ -764,6 +805,7 @@ impl SlabInner { #[cfg(feature = "concurrency")] #[derive(Debug)] +#[must_use] #[allow(clippy::type_complexity)] pub struct ConcurrentSlabStore { inner: RwLock>, @@ -810,11 +852,14 @@ where /// use cachekit::store::slab::ConcurrentSlabStore; /// use cachekit::store::traits::ConcurrentStore; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let store: ConcurrentSlabStore<&str, i32> = ConcurrentSlabStore::new(10); - /// store.try_insert("key", Arc::new(42)).unwrap(); + /// store.try_insert("key", Arc::new(42))?; /// /// let id = store.entry_id(&"key").unwrap(); /// assert!(store.get_by_id(id).is_some()); + /// # Ok(()) + /// # } /// ``` pub fn entry_id(&self, key: &K) -> Option { self.inner.read().index.get(key).copied() @@ -832,12 +877,15 @@ where /// use cachekit::store::slab::ConcurrentSlabStore; /// use cachekit::store::traits::ConcurrentStore; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let store: ConcurrentSlabStore<&str, i32> = ConcurrentSlabStore::new(10); - /// store.try_insert("key", Arc::new(42)).unwrap(); + /// store.try_insert("key", Arc::new(42))?; /// let id = store.entry_id(&"key").unwrap(); /// /// let value: Arc = store.get_by_id(id).unwrap(); /// assert_eq!(*value, 42); + /// # Ok(()) + /// # } /// ``` pub fn get_by_id(&self, id: EntryId) -> Option> { let inner = self.inner.read(); @@ -858,11 +906,14 @@ where /// use cachekit::store::slab::ConcurrentSlabStore; /// use cachekit::store::traits::ConcurrentStore; /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { /// let store: ConcurrentSlabStore = ConcurrentSlabStore::new(10); - /// store.try_insert("my_key".into(), Arc::new(42)).unwrap(); + /// store.try_insert("my_key".into(), Arc::new(42))?; /// let id = store.entry_id(&"my_key".into()).unwrap(); /// /// assert_eq!(store.key_by_id(id), Some("my_key".into())); + /// # Ok(()) + /// # } /// ``` pub fn key_by_id(&self, id: EntryId) -> Option where @@ -875,9 +926,97 @@ where .and_then(|slot| slot.as_ref().map(|entry| entry.key.clone())) } + /// Returns a clone of the value without updating metrics. + /// + /// Acquires read lock. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::slab::ConcurrentSlabStore; + /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead}; + /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { + /// let store: ConcurrentSlabStore<&str, i32> = ConcurrentSlabStore::new(10); + /// store.try_insert("key", Arc::new(42))?; + /// + /// // Peek doesn't update metrics + /// assert_eq!(store.peek(&"key"), Some(Arc::new(42))); + /// assert_eq!(store.metrics().hits, 0); + /// # Ok(()) + /// # } + /// ``` + pub fn peek(&self, key: &K) -> Option> { + let inner = self.inner.read(); + let id = inner.index.get(key)?; + inner + .entries + .get(id.0) + .and_then(|slot| slot.as_ref().map(|entry| Arc::clone(&entry.value))) + } + + /// Removes an entry by `EntryId`, returning the key and value. + /// + /// Enables O(1) eviction when the policy tracks handles. The slot is + /// returned to the free list for reuse. + /// + /// Acquires write lock. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::slab::ConcurrentSlabStore; + /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead}; + /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { + /// let store: ConcurrentSlabStore<&str, i32> = ConcurrentSlabStore::new(10); + /// store.try_insert("key", Arc::new(42))?; + /// let id = store.entry_id(&"key").unwrap(); + /// + /// let (key, value) = store.remove_by_id(id).unwrap(); + /// assert_eq!(key, "key"); + /// assert_eq!(*value, 42); + /// + /// assert!(!store.contains(&"key")); + /// # Ok(()) + /// # } + /// ``` + pub fn remove_by_id(&self, id: EntryId) -> Option<(K, Arc)> { + let mut inner = self.inner.write(); + let entry = inner.entries.get_mut(id.0)?.take()?; + inner.index.remove(&entry.key); + inner.free_list.push(id.0); + self.metrics.inc_remove(); + Some((entry.key, entry.value)) + } + /// Records an eviction in the metrics. /// /// Thread-safe via atomic increment. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::slab::ConcurrentSlabStore; + /// use cachekit::store::traits::{ConcurrentStore, ConcurrentStoreRead}; + /// + /// # fn main() -> Result<(), cachekit::store::traits::StoreFull> { + /// let store: ConcurrentSlabStore<&str, i32> = ConcurrentSlabStore::new(10); + /// store.try_insert("victim", Arc::new(1))?; + /// let id = store.entry_id(&"victim").unwrap(); + /// + /// store.remove_by_id(id); + /// store.record_eviction(); + /// + /// let m = store.metrics(); + /// assert_eq!(m.removes, 1); + /// assert_eq!(m.evictions, 1); + /// # Ok(()) + /// # } + /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } @@ -1119,4 +1258,29 @@ mod tests { assert_eq!(store.get_by_id(id), Some(Arc::new("v1".to_string()))); assert_eq!(store.key_by_id(id), Some("k1")); } + + #[cfg(feature = "concurrency")] + #[test] + fn concurrent_slab_store_remove_by_id() { + let store = ConcurrentSlabStore::new(2); + store.try_insert("k1", Arc::new("v1".to_string())).unwrap(); + let id = store.entry_id(&"k1").unwrap(); + let (key, value) = store.remove_by_id(id).unwrap(); + assert_eq!(key, "k1"); + assert_eq!(*value, "v1".to_string()); + assert!(!store.contains(&"k1")); + } + + #[cfg(feature = "concurrency")] + #[test] + fn concurrent_slab_store_peek() { + let store = ConcurrentSlabStore::new(2); + store.try_insert("k1", Arc::new("v1".to_string())).unwrap(); + + assert_eq!(store.peek(&"k1"), Some(Arc::new("v1".to_string()))); + assert_eq!(store.metrics().hits, 0); + + assert!(store.peek(&"missing").is_none()); + assert_eq!(store.metrics().misses, 0); + } } diff --git a/src/store/traits.rs b/src/store/traits.rs index ca23d18..7b1220b 100644 --- a/src/store/traits.rs +++ b/src/store/traits.rs @@ -90,19 +90,20 @@ use std::sync::Arc; /// ``` /// use cachekit::store::traits::StoreMetrics; /// -/// let metrics = StoreMetrics { -/// hits: 150, -/// misses: 50, -/// inserts: 100, -/// updates: 20, -/// removes: 10, -/// evictions: 40, -/// }; +/// let mut metrics = StoreMetrics::default(); +/// metrics.hits = 150; +/// metrics.misses = 50; +/// metrics.inserts = 100; +/// metrics.updates = 20; +/// metrics.removes = 10; +/// metrics.evictions = 40; /// /// let hit_rate = metrics.hits as f64 / (metrics.hits + metrics.misses) as f64; /// assert!((hit_rate - 0.75).abs() < f64::EPSILON); /// ``` -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] pub struct StoreMetrics { /// Number of successful lookups. pub hits: u64, @@ -137,9 +138,17 @@ pub struct StoreMetrics { /// } /// } /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct StoreFull; +impl std::fmt::Display for StoreFull { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("store is at capacity") + } +} + +impl std::error::Error for StoreFull {} + // ============================================================================= // Single-threaded store traits // ============================================================================= diff --git a/src/store/weight.rs b/src/store/weight.rs index 53402af..3adbf63 100644 --- a/src/store/weight.rs +++ b/src/store/weight.rs @@ -117,13 +117,15 @@ //! ```rust //! use std::sync::Arc; //! use cachekit::store::weight::WeightStore; +//! use cachekit::store::traits::StoreFull; //! +//! # fn main() -> Result<(), StoreFull> { //! // Cache with max 100 entries OR 1MB total, whichever is hit first //! let mut store = WeightStore::with_capacity(100, 1_000_000, |v: &Vec| v.len()); //! //! // Insert entries -//! store.try_insert("small", Arc::new(vec![0u8; 100])).unwrap(); -//! store.try_insert("large", Arc::new(vec![0u8; 10_000])).unwrap(); +//! store.try_insert("small", Arc::new(vec![0u8; 100]))?; +//! store.try_insert("large", Arc::new(vec![0u8; 10_000]))?; //! //! assert_eq!(store.len(), 2); //! assert_eq!(store.total_weight(), 10_100); @@ -131,6 +133,8 @@ //! // Remove adjusts weight //! store.remove(&"large"); //! assert_eq!(store.total_weight(), 100); +//! # Ok(()) +//! # } //! ``` //! //! ## Type Constraints @@ -151,6 +155,7 @@ //! - Does **not** implement `StoreCore`/`StoreMut` (uses `Arc` API) //! - Metrics use atomic counters for concurrent compatibility +use std::fmt; use std::hash::Hash; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; @@ -252,7 +257,9 @@ impl StoreCounters { /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::WeightStore; +/// use cachekit::store::traits::StoreFull; /// +/// # fn main() -> Result<(), StoreFull> { /// // Image cache: max 50 images OR 100MB, whichever is hit first /// let mut store = WeightStore::with_capacity( /// 50, // max entries @@ -264,8 +271,8 @@ impl StoreCounters { /// let small_img = Arc::new(vec![0u8; 1_000]); // 1KB /// let large_img = Arc::new(vec![0u8; 10_000_000]); // 10MB /// -/// store.try_insert("thumbnail", small_img).unwrap(); -/// store.try_insert("fullsize", large_img).unwrap(); +/// store.try_insert("thumbnail", small_img)?; +/// store.try_insert("fullsize", large_img)?; /// /// assert_eq!(store.len(), 2); /// assert_eq!(store.total_weight(), 10_001_000); @@ -276,6 +283,8 @@ impl StoreCounters { /// let _ = store.get(&"missing"); /// assert_eq!(store.metrics().hits, 1); /// assert_eq!(store.metrics().misses, 1); +/// # Ok(()) +/// # } /// ``` /// /// # Weight Function Examples @@ -299,11 +308,7 @@ impl StoreCounters { /// |doc: &Document| doc.content.len() + doc.metadata.len() /// ); /// ``` -#[derive(Debug)] -pub struct WeightStore -where - F: Fn(&V) -> usize, -{ +pub struct WeightStore { map: FxHashMap>, capacity_entries: usize, capacity_weight: usize, @@ -359,19 +364,34 @@ where /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); /// - /// store.try_insert("a", Arc::new("hello".into())).unwrap(); // weight 5 - /// store.try_insert("b", Arc::new("world!".into())).unwrap(); // weight 6 + /// store.try_insert("a", Arc::new("hello".into()))?; // weight 5 + /// store.try_insert("b", Arc::new("world!".into()))?; // weight 6 /// /// assert_eq!(store.total_weight(), 11); + /// # Ok(()) + /// # } /// ``` pub fn total_weight(&self) -> usize { self.total_weight } /// Returns the configured maximum weight capacity. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::weight::WeightStore; + /// + /// let store: WeightStore<&str, String, _> = + /// WeightStore::with_capacity(100, 50_000, |s: &String| s.len()); + /// + /// assert_eq!(store.capacity_weight(), 50_000); + /// ``` pub fn capacity_weight(&self) -> usize { self.capacity_weight } @@ -390,15 +410,19 @@ where /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); - /// store.try_insert("key", Arc::new("value".into())).unwrap(); + /// store.try_insert("key", Arc::new("value".into()))?; /// /// assert_eq!(store.get(&"key"), Some(Arc::new("value".into()))); /// assert_eq!(store.get(&"missing"), None); /// /// assert_eq!(store.metrics().hits, 1); /// assert_eq!(store.metrics().misses, 1); + /// # Ok(()) + /// # } /// ``` pub fn get(&self, key: &K) -> Option> { match self.map.get(key).map(|entry| Arc::clone(&entry.value)) { @@ -420,39 +444,124 @@ where /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); - /// store.try_insert("key", Arc::new("value".into())).unwrap(); + /// store.try_insert("key", Arc::new("value".into()))?; /// /// // Peek doesn't affect metrics /// assert!(store.peek(&"key").is_some()); /// assert_eq!(store.metrics().hits, 0); + /// # Ok(()) + /// # } /// ``` pub fn peek(&self, key: &K) -> Option<&Arc> { self.map.get(key).map(|entry| &entry.value) } /// Returns `true` if the key exists in the store. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// store.try_insert("key", Arc::new("value".into()))?; + /// + /// assert!(store.contains(&"key")); + /// assert!(!store.contains(&"missing")); + /// # Ok(()) + /// # } + /// ``` pub fn contains(&self, key: &K) -> bool { self.map.contains_key(key) } /// Returns the current number of entries. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// assert_eq!(store.len(), 0); + /// + /// store.try_insert("key", Arc::new("value".into()))?; + /// assert_eq!(store.len(), 1); + /// # Ok(()) + /// # } + /// ``` pub fn len(&self) -> usize { self.map.len() } /// Returns `true` if the store contains no entries. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// assert!(store.is_empty()); + /// + /// store.try_insert("key", Arc::new("value".into()))?; + /// assert!(!store.is_empty()); + /// # Ok(()) + /// # } + /// ``` pub fn is_empty(&self) -> bool { self.map.is_empty() } /// Returns the maximum entry capacity. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::weight::WeightStore; + /// + /// let store: WeightStore<&str, String, _> = + /// WeightStore::with_capacity(100, 50_000, |s: &String| s.len()); + /// + /// assert_eq!(store.capacity(), 100); + /// ``` pub fn capacity(&self) -> usize { self.capacity_entries } /// Returns a snapshot of the store's metrics. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// store.try_insert("key", Arc::new("value".into()))?; + /// let _ = store.get(&"key"); + /// + /// let m = store.metrics(); + /// assert_eq!(m.inserts, 1); + /// assert_eq!(m.hits, 1); + /// # Ok(()) + /// # } + /// ``` pub fn metrics(&self) -> StoreMetrics { self.metrics.snapshot() } @@ -461,6 +570,18 @@ where /// /// Call when the policy evicts an entry. Separate from `remove()` to /// distinguish user-initiated removals from policy-driven evictions. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::weight::WeightStore; + /// + /// let store: WeightStore<&str, String, _> = + /// WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// + /// store.record_eviction(); + /// assert_eq!(store.metrics().evictions, 1); + /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } @@ -478,6 +599,10 @@ where /// - Entry count would exceed `capacity_entries` (new key only) /// - Total weight would exceed `capacity_weight` /// + /// # Panics + /// + /// Panics if internal weight accounting is inconsistent (indicates a bug). + /// /// # Example /// /// ``` @@ -485,18 +610,21 @@ where /// use cachekit::store::weight::WeightStore; /// use cachekit::store::traits::StoreFull; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store = WeightStore::with_capacity(10, 100, |s: &String| s.len()); /// /// // Insert succeeds - /// assert!(store.try_insert("a", Arc::new("hello".into())).is_ok()); + /// store.try_insert("a", Arc::new("hello".into()))?; /// /// // Update returns old value - /// let old = store.try_insert("a", Arc::new("hi".into())).unwrap(); + /// let old = store.try_insert("a", Arc::new("hi".into()))?; /// assert_eq!(old, Some(Arc::new("hello".into()))); /// /// // Weight limit exceeded /// let huge = Arc::new("x".repeat(200)); /// assert_eq!(store.try_insert("b", huge), Err(StoreFull)); + /// # Ok(()) + /// # } /// ``` pub fn try_insert(&mut self, key: K, value: Arc) -> Result>, StoreFull> { let new_weight = self.compute_weight(value.as_ref()); @@ -547,18 +675,26 @@ where /// /// Adjusts `total_weight` by subtracting the entry's weight. /// + /// # Panics + /// + /// Panics if internal weight accounting is inconsistent (indicates a bug). + /// /// # Example /// /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; /// + /// # fn main() -> Result<(), StoreFull> { /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); - /// store.try_insert("key", Arc::new("value".into())).unwrap(); + /// store.try_insert("key", Arc::new("value".into()))?; /// /// assert_eq!(store.total_weight(), 5); /// store.remove(&"key"); /// assert_eq!(store.total_weight(), 0); + /// # Ok(()) + /// # } /// ``` pub fn remove(&mut self, key: &K) -> Option> { let entry = self.map.remove(key)?; @@ -575,12 +711,42 @@ where } /// Removes all entries and resets total weight to zero. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; + /// use cachekit::store::weight::WeightStore; + /// use cachekit::store::traits::StoreFull; + /// + /// # fn main() -> Result<(), StoreFull> { + /// let mut store = WeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// store.try_insert("a", Arc::new("hello".into()))?; + /// store.try_insert("b", Arc::new("world".into()))?; + /// + /// store.clear(); + /// assert!(store.is_empty()); + /// assert_eq!(store.total_weight(), 0); + /// # Ok(()) + /// # } + /// ``` pub fn clear(&mut self) { self.map.clear(); self.total_weight = 0; } } +impl fmt::Debug for WeightStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WeightStore") + .field("len", &self.map.len()) + .field("capacity_entries", &self.capacity_entries) + .field("capacity_weight", &self.capacity_weight) + .field("total_weight", &self.total_weight) + .finish_non_exhaustive() + } +} + // ============================================================================= // Concurrent WeightStore // ============================================================================= @@ -629,12 +795,8 @@ where /// assert_eq!(store.len(), 100); /// assert_eq!(store.total_weight(), 10_000); // 100 entries × 100 bytes /// ``` -#[derive(Debug)] #[cfg(feature = "concurrency")] -pub struct ConcurrentWeightStore -where - F: Fn(&V) -> usize, -{ +pub struct ConcurrentWeightStore { inner: RwLock>, metrics: StoreCounters, } @@ -689,12 +851,15 @@ where /// ``` /// use std::sync::Arc; /// use cachekit::store::weight::ConcurrentWeightStore; - /// use cachekit::store::traits::ConcurrentStore; + /// use cachekit::store::traits::{ConcurrentStore, StoreFull}; /// + /// # fn main() -> Result<(), StoreFull> { /// let store = ConcurrentWeightStore::with_capacity(10, 1000, |s: &String| s.len()); - /// store.try_insert("key", Arc::new("hello".into())).unwrap(); + /// store.try_insert("key", Arc::new("hello".into()))?; /// /// assert_eq!(store.total_weight(), 5); + /// # Ok(()) + /// # } /// ``` pub fn total_weight(&self) -> usize { self.inner.read().total_weight() @@ -703,6 +868,18 @@ where /// Returns the configured maximum weight capacity. /// /// Acquires read lock. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::weight::ConcurrentWeightStore; + /// use cachekit::store::traits::ConcurrentStoreRead; + /// + /// let store: ConcurrentWeightStore = + /// ConcurrentWeightStore::with_capacity(100, 50_000, |s: &String| s.len()); + /// + /// assert_eq!(store.capacity_weight(), 50_000); + /// ``` pub fn capacity_weight(&self) -> usize { self.inner.read().capacity_weight() } @@ -710,11 +887,37 @@ where /// Records an eviction in the metrics. /// /// Thread-safe via atomic increment. + /// + /// # Example + /// + /// ``` + /// use cachekit::store::weight::ConcurrentWeightStore; + /// use cachekit::store::traits::ConcurrentStoreRead; + /// + /// let store: ConcurrentWeightStore = + /// ConcurrentWeightStore::with_capacity(10, 1000, |s: &String| s.len()); + /// + /// store.record_eviction(); + /// assert_eq!(store.metrics().evictions, 1); + /// ``` pub fn record_eviction(&self) { self.metrics.inc_eviction(); } } +#[cfg(feature = "concurrency")] +impl fmt::Debug for ConcurrentWeightStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self.inner.read(); + f.debug_struct("ConcurrentWeightStore") + .field("len", &inner.map.len()) + .field("capacity_entries", &inner.capacity_entries) + .field("capacity_weight", &inner.capacity_weight) + .field("total_weight", &inner.total_weight) + .finish_non_exhaustive() + } +} + /// Read operations for [`ConcurrentWeightStore`]. /// /// All methods acquire a read lock on the inner store.