diff --git a/README.md b/README.md index a248810..eaf9035 100644 --- a/README.md +++ b/README.md @@ -74,58 +74,82 @@ let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7); println!("Recommended capacity: {}", recommended); ``` -## Thread-Safe Implementations - -These implementations are available when using the appropriate feature flags: -- `SyncSieveCache` is available with the `sync` feature (enabled by default) -- `ShardedSieveCache` is available with the `sharded` feature (enabled by default) +### Custom Hash Builder -### `SyncSieveCache` - Basic Thread-Safe Cache - -For concurrent access from multiple threads, you can use the `SyncSieveCache` wrapper, which provides thread safety with a single global lock: +You can also specify a custom hash builder to use a different hashing algorithm (e.g., `ahash` for faster hashing): ```rust -use sieve_cache::SyncSieveCache; -use std::thread; - -// Create a thread-safe cache -let cache = SyncSieveCache::new(100000).unwrap(); - -// The cache can be safely cloned and shared between threads -let cache_clone = cache.clone(); +use sieve_cache::SieveCache; +use std::hash::BuildHasherDefault; +use std::collections::hash_map::DefaultHasher; -// Insert from the main thread -cache.insert("foo".to_string(), "foocontent".to_string()); +// Create a cache with a custom hash builder +let hasher = BuildHasherDefault::::default(); +let mut cache: SieveCache = SieveCache::new_with_hasher(100000, hasher).unwrap(); -// Access from another thread -let handle = thread::spawn(move || { - // Insert a new key - cache_clone.insert("bar".to_string(), "barcontent".to_string()); +// Use the cache normally +cache.insert("key".to_string(), "value".to_string()); +assert_eq!(cache.get("key"), Some(&"value".to_string())); +``` - // Get returns a clone of the value, not a reference (unlike non-thread-safe version) - assert_eq!(cache_clone.get(&"foo".to_string()), Some("foocontent".to_string())); -}); +This feature is useful when you need to optimize hashing performance for specific workloads or integrate with crates like `ahash`. -// Wait for the thread to complete -handle.join().unwrap(); +## Thread-Safe Implementations -// Check if keys exist -assert!(cache.contains_key(&"foo".to_string())); -assert!(cache.contains_key(&"bar".to_string())); +These implementations are available when using the appropriate feature flags: +- `SyncSieveCache` is available with the `sync` feature (enabled by default) +- `ShardedSieveCache` is available with the `sharded` feature (enabled by default) -// Remove an entry -let removed = cache.remove(&"bar".to_string()); -assert_eq!(removed, Some("barcontent".to_string())); +### `SyncSieveCache` - Basic Thread-Safe Cache -// Perform multiple operations atomically with exclusive access -cache.with_lock(|inner_cache| { - // Operations inside this closure have exclusive access to the cache - inner_cache.insert("atomic1".to_string(), "value1".to_string()); - inner_cache.insert("atomic2".to_string(), "value2".to_string()); +For concurrent access from multiple threads, you can use the `SyncSieveCache` wrapper, which provides thread safety with a single global lock: - // We can check internal state as part of the transaction - assert_eq!(inner_cache.len(), 3); -}); +```rust +// the line below is required for doctests, you will rarely need to wrap code like this. +# #[cfg(feature = "sync")] +{ + use sieve_cache::SyncSieveCache; + use std::thread; + + // Create a thread-safe cache + let cache = SyncSieveCache::new(100000).unwrap(); + + // The cache can be safely cloned and shared between threads + let cache_clone = cache.clone(); + + // Insert from the main thread + cache.insert("foo".to_string(), "foocontent".to_string()); + + // Access from another thread + let handle = thread::spawn(move || { + // Insert a new key + cache_clone.insert("bar".to_string(), "barcontent".to_string()); + + // Get returns a clone of the value, not a reference (unlike non-thread-safe version) + assert_eq!(cache_clone.get(&"foo".to_string()), Some("foocontent".to_string())); + }); + + // Wait for the thread to complete + handle.join().unwrap(); + + // Check if keys exist + assert!(cache.contains_key(&"foo".to_string())); + assert!(cache.contains_key(&"bar".to_string())); + + // Remove an entry + let removed = cache.remove(&"bar".to_string()); + assert_eq!(removed, Some("barcontent".to_string())); + + // Perform multiple operations atomically with exclusive access + cache.with_lock(|inner_cache| { + // Operations inside this closure have exclusive access to the cache + inner_cache.insert("atomic1".to_string(), "value1".to_string()); + inner_cache.insert("atomic2".to_string(), "value2".to_string()); + + // We can check internal state as part of the transaction + assert_eq!(inner_cache.len(), 3); + }); +} ``` Key differences from the non-thread-safe version: @@ -138,59 +162,63 @@ Key differences from the non-thread-safe version: For applications with high concurrency requirements, the `ShardedSieveCache` implementation uses multiple internal locks (sharding) to reduce contention and improve throughput: ```rust -use sieve_cache::ShardedSieveCache; -use std::thread; -use std::sync::Arc; - -// Create a sharded cache with default shard count (16) -// We use Arc for sharing between threads -let cache = Arc::new(ShardedSieveCache::new(100000).unwrap()); - -// Alternatively, specify a custom number of shards -// let cache = Arc::new(ShardedSieveCache::with_shards(100000, 32).unwrap()); - -// Insert data from the main thread -cache.insert("foo".to_string(), "foocontent".to_string()); - -// Use multiple worker threads to insert data concurrently -let mut handles = vec![]; -for i in 0..8 { - let cache_clone = Arc::clone(&cache); - let handle = thread::spawn(move || { - // Each thread inserts multiple values - for j in 0..100 { - let key = format!("key_thread{}_item{}", i, j); - let value = format!("value_{}", j); - cache_clone.insert(key, value); - } +// the line below is required for doctests, you will rarely need to wrap code like this. +# #[cfg(feature = "sharded")] +{ + use sieve_cache::ShardedSieveCache; + use std::thread; + use std::sync::Arc; + + // Create a sharded cache with default shard count (16) + // We use Arc for sharing between threads + let cache = Arc::new(ShardedSieveCache::new(100000).unwrap()); + + // Alternatively, specify a custom number of shards + // let cache = Arc::new(ShardedSieveCache::with_shards(100000, 32).unwrap()); + + // Insert data from the main thread + cache.insert("foo".to_string(), "foocontent".to_string()); + + // Use multiple worker threads to insert data concurrently + let mut handles = vec![]; + for i in 0..8 { + let cache_clone = Arc::clone(&cache); + let handle = thread::spawn(move || { + // Each thread inserts multiple values + for j in 0..100 { + let key = format!("key_thread{}_item{}", i, j); + let value = format!("value_{}", j); + cache_clone.insert(key, value); + } + }); + handles.push(handle); + } + + // Wait for all threads to complete + for handle in handles { + handle.join().unwrap(); + } + + // Shard-specific atomic operations + // with_key_lock locks only the shard containing the key + cache.with_key_lock(&"foo", |shard| { + // Operations inside this closure have exclusive access to the specific shard + shard.insert("related_key1".to_string(), "value1".to_string()); + shard.insert("related_key2".to_string(), "value2".to_string()); + + // We can check internal state within the transaction + assert!(shard.contains_key(&"related_key1".to_string())); }); - handles.push(handle); -} - -// Wait for all threads to complete -for handle in handles { - handle.join().unwrap(); + + // Get the number of entries across all shards + // Note: This acquires all shard locks sequentially + let total_entries = cache.len(); + assert_eq!(total_entries, 803); // 800 from threads + 1 "foo" + 2 related keys + + // Access shard information + println!("Cache has {} shards with total capacity {}", + cache.num_shards(), cache.capacity()); } - -// Shard-specific atomic operations -// with_key_lock locks only the shard containing the key -cache.with_key_lock(&"foo", |shard| { - // Operations inside this closure have exclusive access to the specific shard - shard.insert("related_key1".to_string(), "value1".to_string()); - shard.insert("related_key2".to_string(), "value2".to_string()); - - // We can check internal state within the transaction - assert!(shard.contains_key(&"related_key1".to_string())); -}); - -// Get the number of entries across all shards -// Note: This acquires all shard locks sequentially -let total_entries = cache.len(); -assert_eq!(total_entries, 803); // 800 from threads + 1 "foo" + 2 related keys - -// Access shard information -println!("Cache has {} shards with total capacity {}", - cache.num_shards(), cache.capacity()); ``` #### How Sharding Works @@ -210,24 +238,28 @@ The `weighted` feature (enabled by default) adds cache implementations that evic ### `WeightedSieveCache` - Single-Threaded ```rust -use sieve_cache::{Weigh, WeightedSieveCache}; - -// Create a cache that holds at most 10 entries and 1 KB of weight -let mut cache = WeightedSieveCache::new(10, 1024).unwrap(); - -cache.insert("key".to_string(), "value".to_string()); -assert!(cache.current_weight() > 0); -assert!(cache.current_weight() <= cache.max_weight()); - -// Weight is snapshotted at insert time. -// Mutating via get_mut does not change the tracked weight. -if let Some(v) = cache.get_mut("key") { - *v = "a much longer string".to_string(); +// the line below is required for doctests, you will rarely need to wrap code like this. +# #[cfg(feature = "weighted")] +{ + use sieve_cache::{Weigh, WeightedSieveCache}; + + // Create a cache that holds at most 10 entries and 1 KB of weight + let mut cache = WeightedSieveCache::new(10, 1024).unwrap(); + + cache.insert("key".to_string(), "value".to_string()); + assert!(cache.current_weight() > 0); + assert!(cache.current_weight() <= cache.max_weight()); + + // Weight is snapshotted at insert time. + // Mutating via get_mut does not change the tracked weight. + if let Some(v) = cache.get_mut("key") { + *v = "a much longer string".to_string(); + } + // current_weight still reflects the original "value" + + // Entries are evicted automatically when inserting would exceed max_weight. + // A single oversized entry is allowed as the sole occupant. } -// current_weight still reflects the original "value" - -// Entries are evicted automatically when inserting would exceed max_weight. -// A single oversized entry is allowed as the sole occupant. ``` Thread-safe variants follow the same pattern as their non-weighted counterparts: diff --git a/src/lib.rs b/src/lib.rs index d21c66e..e5ec19c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,9 +78,9 @@ pub mod _docs_sharded_usage { //! are distributed across many different keys } -use std::borrow::Borrow; use std::collections::HashMap; -use std::hash::Hash; +use std::hash::{Hash, RandomState}; +use std::{borrow::Borrow, hash::BuildHasher}; #[cfg(feature = "sharded")] mod sharded; @@ -136,6 +136,7 @@ impl Node { /// /// * `K` - The key type, which must implement `Eq`, `Hash`, and `Clone` /// * `V` - The value type, with no constraints +/// * `S` - The hash builder type for the underlying `HashMap` (default: `RandomState`). Must implement `BuildHasher` /// /// # Example /// @@ -171,9 +172,9 @@ impl Node { /// assert_eq!(removed, Some("value2".to_string())); /// # } /// ``` -pub struct SieveCache { +pub struct SieveCache { /// Map of keys to indices in the nodes vector - map: HashMap, + map: HashMap, /// Vector of all cache nodes nodes: Vec>, /// Index to the "hand" pointer used by the SIEVE algorithm for eviction @@ -209,17 +210,82 @@ impl SieveCache { /// # } /// ``` pub fn new(capacity: usize) -> Result { + Self::new_with_hasher(capacity, Default::default()) + } +} + +impl SieveCache { + /// Creates a new cache with the given capacity using a custom hash builder. + /// + /// The capacity represents the maximum number of key-value pairs + /// that can be stored in the cache. When this limit is reached, + /// the cache will use the SIEVE algorithm to evict entries. + /// + /// # Arguments + /// + /// * `capacity` - The maximum number of entries in the cache + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// # Errors + /// + /// Returns an error if the capacity is zero. + /// + /// # Examples + /// + /// ```rust + /// # #[cfg(feature = "doctest")] + /// # { + /// use sieve_cache::SieveCache; + /// use std::hash::BuildHasherDefault; + /// use std::collections::hash_map::DefaultHasher; + /// + /// // Create a cache with a custom hasher + /// let hasher = BuildHasherDefault::::default(); + /// let cache: SieveCache = SieveCache::new_with_hasher(100, hasher).unwrap(); + /// assert_eq!(cache.capacity(), 100); + /// # } + /// ``` + pub fn new_with_hasher(capacity: usize, hasher: S) -> Result { if capacity == 0 { return Err("capacity must be greater than 0"); } Ok(Self { - map: HashMap::with_capacity(capacity), + map: HashMap::with_capacity_and_hasher(capacity, hasher), nodes: Vec::with_capacity(capacity), hand: None, capacity, }) } +} +// hasher() can only be implemented if the BuildHasher is also Clone, +// since copies of mere references will be dropped by the MutexGuard in shared variants. +impl SieveCache { + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to a sharded or thread-safe variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ```rust + /// # #[cfg(feature = "doctest")] + /// # { + /// use sieve_cache::SieveCache; + /// use std::hash::BuildHasherDefault; + /// use std::collections::hash_map::DefaultHasher; + /// + /// let hasher = BuildHasherDefault::::default(); + /// let cache: SieveCache = SieveCache::new_with_hasher(100, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// # } + /// ``` + pub fn hasher(&self) -> S { + (self.map.hasher()).clone() + } +} + +impl SieveCache { /// Returns the capacity of the cache. /// /// This is the maximum number of entries that can be stored before @@ -1357,3 +1423,31 @@ fn insert_never_exceeds_capacity_when_all_visited() { // This is our an invariant assert!(c.len() <= c.capacity()); } + +#[test] +fn test_custom_hasher_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let mut cache: SieveCache = SieveCache::new_with_hasher(10, hasher).unwrap(); + + // Test basic insert and get operations with custom hasher + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + assert!(cache.insert("key3".to_string(), "value3".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get("key1"), Some(&"value1".to_string())); + assert_eq!(cache.get("key2"), Some(&"value2".to_string())); + assert_eq!(cache.get("key3"), Some(&"value3".to_string())); + + // Test remove with custom hasher + assert_eq!(cache.remove("key1"), Some("value1".to_string())); + assert_eq!(cache.get("key1"), None); + assert_eq!(cache.len(), 2); + + // Test contains_key + assert!(cache.contains_key("key2")); + assert!(!cache.contains_key("key1")); +} diff --git a/src/sharded.rs b/src/sharded.rs index 9d4e434..ba22db7 100644 --- a/src/sharded.rs +++ b/src/sharded.rs @@ -1,8 +1,7 @@ use crate::SieveCache; use std::borrow::Borrow; -use std::collections::hash_map::DefaultHasher; use std::fmt; -use std::hash::{Hash, Hasher}; +use std::hash::{BuildHasher, Hash, Hasher, RandomState}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; /// Default number of shards to use if not specified explicitly. @@ -69,6 +68,12 @@ const DEFAULT_SHARDS: usize = 16; /// sharding may be limited /// - Default of 16 shards provides a good balance for most workloads, but can be customized /// +/// # Type Parameters +/// +/// * `K` - The key type, which must implement `Eq`, `Hash`, and `Clone` + `Send` + `Sync` +/// * `V` - The value type, must implement `Send` + `Sync` +/// * `S` - The hash builder type for the underlying `HashMap` (default: `RandomState`). Must implement `BuildHasher` +/// /// # Examples /// /// ``` @@ -116,21 +121,27 @@ const DEFAULT_SHARDS: usize = 16; /// assert_eq!(cache.len(), 400); // All 400 items were inserted /// ``` #[derive(Clone)] -pub struct ShardedSieveCache +pub struct ShardedSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Send + Sync, + S: BuildHasher + Clone, { /// Array of shard mutexes, each containing a separate SieveCache instance - shards: Vec>>>, + shards: Vec>>>, /// Number of shards in the cache - kept as a separate field for quick access num_shards: usize, + /// The hash builder used for sharding keys, cloned into each shard's SieveCache + hasher: S, + // this is stored redundantly here to increase performance of get_shard_index() + // by avoiding a lock on the shard to access its hasher } -impl Default for ShardedSieveCache +impl Default for ShardedSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Send + Sync, + S: BuildHasher + Clone + Default, { /// Creates a new sharded cache with a default capacity of 100 entries and default number of shards. /// @@ -149,14 +160,16 @@ where /// assert_eq!(cache.num_shards(), 16); // Default shard count /// ``` fn default() -> Self { - Self::new(100).expect("Failed to create cache with default capacity") + Self::new_with_hasher(100, Default::default()) + .expect("Failed to create cache with default capacity") } } -impl fmt::Debug for ShardedSieveCache +impl fmt::Debug for ShardedSieveCache where K: Eq + Hash + Clone + Send + Sync + fmt::Debug, V: Send + Sync + fmt::Debug, + S: BuildHasher + Clone, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ShardedSieveCache") @@ -167,10 +180,11 @@ where } } -impl IntoIterator for ShardedSieveCache +impl IntoIterator for ShardedSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Clone + Send + Sync, + S: BuildHasher + Clone, { type Item = (K, V); type IntoIter = std::vec::IntoIter<(K, V)>; @@ -199,14 +213,16 @@ where } #[cfg(feature = "sync")] -impl From> for ShardedSieveCache +impl From> for ShardedSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Clone + Send + Sync, + S: BuildHasher + Clone, { /// Creates a new sharded cache from an existing `SyncSieveCache`. /// /// This allows for upgrading a standard thread-safe cache to a more scalable sharded version. + /// The custom hasher (if any) is preserved from the original cache. /// /// # Examples /// @@ -219,10 +235,14 @@ where /// let sharded_cache = ShardedSieveCache::from(sync_cache); /// assert_eq!(sharded_cache.get(&"key".to_string()), Some("value".to_string())); /// ``` - fn from(sync_cache: crate::SyncSieveCache) -> Self { - // Create a new sharded cache with the same capacity + fn from(sync_cache: crate::SyncSieveCache) -> Self { + // Extract the hasher from the inner cache before consuming the sync cache + let hasher = sync_cache.hasher(); + + // Create a new sharded cache with the same capacity and hasher let capacity = sync_cache.capacity(); - let sharded = Self::new(capacity).expect("Failed to create sharded cache"); + let sharded = + Self::new_with_hasher(capacity, hasher).expect("Failed to create sharded cache"); // Transfer all entries for (key, value) in sync_cache.entries() { @@ -289,6 +309,83 @@ where /// assert!(cache.capacity() >= 1000); /// ``` pub fn with_shards(capacity: usize, num_shards: usize) -> Result { + Self::with_shards_and_hasher(capacity, num_shards, Default::default()) + } +} + +impl ShardedSieveCache +where + K: Eq + Hash + Clone + Send + Sync, + V: Send + Sync, + S: BuildHasher + Clone, +{ + /// Creates a new sharded cache with the specified capacity using a custom hash builder and default number of shards. + /// + /// The capacity will be divided evenly among the shards. The default shard count (16) + /// provides a good balance between concurrency and memory overhead for most workloads. + /// + /// # Arguments + /// + /// * `capacity` - The total capacity of the cache + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// # Errors + /// + /// Returns an error if the capacity is zero. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::ShardedSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: ShardedSieveCache = ShardedSieveCache::new_with_hasher(1000, hasher).unwrap(); + /// assert_eq!(cache.num_shards(), 16); // Default shard count + /// ``` + pub fn new_with_hasher(capacity: usize, hasher: S) -> Result { + Self::with_shards_and_hasher(capacity, DEFAULT_SHARDS, hasher) + } + + /// Creates a new sharded cache with the specified capacity, number of shards, and custom hash builder. + /// + /// The capacity will be divided among the shards, distributing any remainder to ensure + /// the total capacity is at least the requested amount. + /// + /// # Arguments + /// + /// * `capacity` - The total capacity of the cache + /// * `num_shards` - The number of shards to divide the cache into + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// # Errors + /// + /// Returns an error if either the capacity or number of shards is zero. + /// + /// # Performance Impact + /// + /// - More shards can reduce contention in highly concurrent environments + /// - However, each shard has memory overhead, so very high shard counts may + /// increase memory usage without providing additional performance benefits + /// - For most workloads, a value between 8 and 32 shards is optimal + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::ShardedSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// // Create a cache with 8 shards and a custom hasher + /// let hasher = BuildHasherDefault::::default(); + /// let cache: ShardedSieveCache = ShardedSieveCache::with_shards_and_hasher(1000, 8, hasher).unwrap(); + /// assert_eq!(cache.num_shards(), 8); + /// assert!(cache.capacity() >= 1000); + /// ``` + pub fn with_shards_and_hasher( + capacity: usize, + num_shards: usize, + hasher: S, + ) -> Result { if capacity == 0 { return Err("capacity must be greater than 0"); } @@ -311,10 +408,36 @@ where // Ensure at least capacity 1 per shard let shard_capacity = std::cmp::max(1, shard_capacity); - shards.push(Arc::new(Mutex::new(SieveCache::new(shard_capacity)?))); + shards.push(Arc::new(Mutex::new(SieveCache::new_with_hasher( + shard_capacity, + hasher.clone(), + )?))); } - Ok(Self { shards, num_shards }) + Ok(Self { + shards, + num_shards, + hasher, + }) + } + + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to another cache variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::ShardedSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: ShardedSieveCache = ShardedSieveCache::with_shards_and_hasher(100, 4, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// ``` + pub fn hasher(&self) -> S { + self.hasher.clone() } /// Returns the shard index for a given key. @@ -326,7 +449,7 @@ where where Q: Hash + ?Sized, { - let mut hasher = DefaultHasher::new(); + let mut hasher = self.hasher.build_hasher(); key.hash(&mut hasher); let hash = hasher.finish() as usize; hash % self.num_shards @@ -336,7 +459,7 @@ where /// /// This is an internal helper method that maps a key to its corresponding shard. #[inline] - fn get_shard(&self, key: &Q) -> &Arc>> + fn get_shard(&self, key: &Q) -> &Arc>> where Q: Hash + ?Sized, { @@ -350,7 +473,7 @@ where /// If the mutex is poisoned due to a panic in another thread, the poison error is /// recovered from by calling `into_inner()` to access the underlying data. #[inline] - fn locked_shard(&self, key: &Q) -> MutexGuard<'_, SieveCache> + fn locked_shard(&self, key: &Q) -> MutexGuard<'_, SieveCache> where Q: Hash + ?Sized, { @@ -946,7 +1069,7 @@ where pub fn with_key_lock(&self, key: &Q, f: F) -> T where Q: Hash + ?Sized, - F: FnOnce(&mut SieveCache) -> T, + F: FnOnce(&mut SieveCache) -> T, { let mut guard = self.locked_shard(key); f(&mut guard) @@ -982,7 +1105,7 @@ where /// // Out of bounds index /// assert!(cache.get_shard_by_index(100).is_none()); /// ``` - pub fn get_shard_by_index(&self, index: usize) -> Option<&Arc>>> { + pub fn get_shard_by_index(&self, index: usize) -> Option<&Arc>>> { self.shards.get(index) } @@ -1699,4 +1822,63 @@ mod tests { assert_eq!(cache.get(&"keyB_2".to_string()), Some(22)); // 2 + 20 assert_eq!(cache.get(&"keyC_3".to_string()), Some(3)); } + + #[test] + fn test_custom_hasher_sharded_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + // Test with custom hasher and default shard count + let cache: ShardedSieveCache = + ShardedSieveCache::new_with_hasher(10, BuildHasherDefault::::new()) + .unwrap(); + + // Test basic insert operations + assert!(cache.insert("a".to_string(), 1)); + assert!(cache.insert("b".to_string(), 2)); + assert!(cache.insert("c".to_string(), 3)); + + // Verify values can be retrieved + assert_eq!(cache.get(&"a".to_string()), Some(1)); + assert_eq!(cache.get(&"b".to_string()), Some(2)); + assert_eq!(cache.get(&"c".to_string()), Some(3)); + + // Test remove + assert_eq!(cache.remove(&"a".to_string()), Some(1)); + assert_eq!(cache.get(&"a".to_string()), None); + + // Test contains + assert!(cache.contains_key(&"b".to_string())); + assert!(!cache.contains_key(&"a".to_string())); + } + + #[test] + fn test_custom_hasher_sharded_sievecache_with_shards() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let cache: ShardedSieveCache = + ShardedSieveCache::with_shards_and_hasher(10, 4, hasher).unwrap(); + + // Test insert and get with custom hasher and custom shard count + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + assert!(cache.insert("key3".to_string(), "value3".to_string())); + assert!(cache.insert("key4".to_string(), "value4".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); + assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string())); + assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string())); + assert_eq!(cache.get(&"key4".to_string()), Some("value4".to_string())); + + // Test remove and contains_key + assert_eq!( + cache.remove(&"key2".to_string()), + Some("value2".to_string()) + ); + assert!(!cache.contains_key(&"key2".to_string())); + assert_eq!(cache.len(), 3); + } } diff --git a/src/sync.rs b/src/sync.rs index 614b5a0..8df4a59 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,7 @@ use crate::SieveCache; use std::borrow::Borrow; use std::fmt; -use std::hash::Hash; +use std::hash::{BuildHasher, Hash, RandomState}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; /// A thread-safe wrapper around `SieveCache`. @@ -85,18 +85,20 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; /// assert_eq!(cache.get(&"key2".to_string()), Some(12)); /// ``` #[derive(Clone)] -pub struct SyncSieveCache +pub struct SyncSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Send + Sync, + S: BuildHasher, { - inner: Arc>>, + inner: Arc>>, } -impl Default for SyncSieveCache +impl Default for SyncSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Send + Sync, + S: BuildHasher + Default, { /// Creates a new cache with a default capacity of 100 entries. /// @@ -114,14 +116,16 @@ where /// assert_eq!(cache.capacity(), 100); /// ``` fn default() -> Self { - Self::new(100).expect("Failed to create cache with default capacity") + Self::new_with_hasher(100, Default::default()) + .expect("Failed to create cache with default capacity") } } -impl fmt::Debug for SyncSieveCache +impl fmt::Debug for SyncSieveCache where K: Eq + Hash + Clone + Send + Sync + fmt::Debug, V: Send + Sync + fmt::Debug, + S: BuildHasher, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let guard = self.locked_cache(); @@ -132,10 +136,11 @@ where } } -impl From> for SyncSieveCache +impl From> for SyncSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Send + Sync, + S: BuildHasher, { /// Creates a new thread-safe cache from an existing `SieveCache`. /// @@ -152,17 +157,18 @@ where /// let thread_safe = SyncSieveCache::from(single_threaded); /// assert_eq!(thread_safe.get(&"key".to_string()), Some("value".to_string())); /// ``` - fn from(cache: SieveCache) -> Self { + fn from(cache: SieveCache) -> Self { Self { inner: Arc::new(Mutex::new(cache)), } } } -impl IntoIterator for SyncSieveCache +impl IntoIterator for SyncSieveCache where K: Eq + Hash + Clone + Send + Sync, V: Clone + Send + Sync, + S: BuildHasher, { type Item = (K, V); type IntoIter = std::vec::IntoIter<(K, V)>; @@ -208,7 +214,38 @@ where /// let cache = SyncSieveCache::::new(100).unwrap(); /// ``` pub fn new(capacity: usize) -> Result { - let cache = SieveCache::new(capacity)?; + Self::new_with_hasher(capacity, Default::default()) + } +} + +impl SyncSieveCache +where + K: Eq + Hash + Clone + Send + Sync, + V: Send + Sync, + S: BuildHasher, +{ + /// Creates a new thread-safe cache with the given capacity using a custom hash builder. + /// + /// # Arguments + /// + /// * `capacity` - The maximum number of entries in the cache + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// # Errors + /// + /// Returns an error if the capacity is zero. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::SyncSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: SyncSieveCache = SyncSieveCache::new_with_hasher(100, hasher).unwrap(); + /// ``` + pub fn new_with_hasher(capacity: usize, hasher: S) -> Result { + let cache = SieveCache::new_with_hasher(capacity, hasher)?; Ok(Self { inner: Arc::new(Mutex::new(cache)), }) @@ -220,7 +257,7 @@ where /// If the mutex is poisoned due to a panic in another thread, the poison /// error is recovered from by calling `into_inner()` to access the underlying data. #[inline] - fn locked_cache(&self) -> MutexGuard<'_, SieveCache> { + fn locked_cache(&self) -> MutexGuard<'_, SieveCache> { self.inner.lock().unwrap_or_else(PoisonError::into_inner) } @@ -738,7 +775,7 @@ where /// ``` pub fn with_lock(&self, f: F) -> T where - F: FnOnce(&mut SieveCache) -> T, + F: FnOnce(&mut SieveCache) -> T, { let mut guard = self.locked_cache(); f(&mut guard) @@ -958,6 +995,34 @@ where } } +// only if the hasher is cloneable, since a reference's lifetime is bound to the MutexGuard +// which is dropped at the end of the block. +impl SyncSieveCache +where + K: Eq + Hash + Clone + Send + Sync, + V: Send + Sync, + S: BuildHasher + Clone, +{ + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to a sharded variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::SyncSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: SyncSieveCache = SyncSieveCache::new_with_hasher(100, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// ``` + pub fn hasher(&self) -> S { + self.locked_cache().hasher() + } +} + #[cfg(test)] mod tests { use super::*; @@ -1077,6 +1142,35 @@ mod tests { assert_eq!(cache.get(&"key2".to_string()), None); } + #[test] + fn test_custom_hasher_sync_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let cache: SyncSieveCache = + SyncSieveCache::new_with_hasher(3, hasher).unwrap(); + + // Test thread-safe insert and get with custom hasher + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + assert!(cache.insert("key3".to_string(), "value3".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); + assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string())); + assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string())); + + // Test remove with custom hasher + cache.remove(&"key1".to_string()); + assert_eq!(cache.get(&"key1".to_string()), None); + assert_eq!(cache.len(), 2); + + // Test contains_key + assert!(cache.contains_key(&"key2".to_string())); + assert!(!cache.contains_key(&"key1".to_string())); + } + #[test] fn test_keys_values_entries() { let cache = SyncSieveCache::new(10).unwrap(); diff --git a/src/weighted.rs b/src/weighted.rs index 6ac7a6f..ff03cac 100644 --- a/src/weighted.rs +++ b/src/weighted.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::hash::Hash; +use std::hash::{BuildHasher, Hash, RandomState}; use std::mem; use crate::SieveCache; @@ -108,18 +108,25 @@ impl Weigh for &[u8] { /// like moka and quick_cache. Users who need accurate tracking after /// mutation can `remove` + `insert`. /// +/// # Type Parameters +/// +/// * `K` - The key type, which must implement `Eq`, `Hash`, `Clone`, and `Weigh` +/// * `V` - The value type, must implement `Weigh` +/// * `S` - The hash builder type for the underlying `HashMap` (default: `RandomState`). Must implement `BuildHasher` +/// /// # Weight overshoot /// /// When a single entry's weight exceeds `max_weight`, the eviction loop /// empties the cache and then inserts the oversized entry as the sole /// occupant. This means `current_weight` can temporarily exceed /// `max_weight` by at most the weight of one entry. -pub struct WeightedSieveCache { - inner: SieveCache, +pub struct WeightedSieveCache +{ + inner: SieveCache, /// Per-entry charged weight, snapshotted at insert time. This is the /// weight that will be subtracted when the entry is removed or evicted, /// regardless of any mutations via `get_mut`. - charged: HashMap, + charged: HashMap, max_weight: usize, current_weight: usize, } @@ -133,18 +140,64 @@ impl WeightedSieveCache { /// /// Returns `Err` if `capacity` is 0 or `max_weight` is 0. pub fn new(capacity: usize, max_weight: usize) -> Result { + Self::new_with_hasher(capacity, max_weight, Default::default()) + } +} + +impl WeightedSieveCache { + /// Creates a new weighted cache using a custom hash builder. + /// + /// `capacity` is the maximum number of entries (passed to the inner + /// `SieveCache`). `max_weight` is the memory budget in bytes — the + /// cache will evict entries to stay at or below this limit. + /// + /// # Arguments + /// + /// * `capacity` - The maximum number of entries in the cache + /// * `max_weight` - The memory budget in bytes + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// Returns `Err` if `capacity` is 0 or `max_weight` is 0. + pub fn new_with_hasher( + capacity: usize, + max_weight: usize, + hasher: S, + ) -> Result { if max_weight == 0 { return Err("max_weight must be greater than zero"); } - let inner = SieveCache::new(capacity)?; + let inner = SieveCache::new_with_hasher(capacity, hasher.clone())?; Ok(Self { inner, - charged: HashMap::new(), + charged: HashMap::with_capacity_and_hasher(capacity, hasher), max_weight, current_weight: 0, }) } + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to another cache variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ```rust + /// # #[cfg(feature = "doctest")] + /// # { + /// use sieve_cache::WeightedSieveCache; + /// use std::hash::BuildHasherDefault; + /// use std::collections::hash_map::DefaultHasher; + /// + /// let hasher = BuildHasherDefault::::default(); + /// let cache: WeightedSieveCache = WeightedSieveCache::new_with_hasher(100, 1000, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// # } + /// ``` + pub fn hasher(&self) -> S { + self.inner.hasher() + } + /// Inserts a key-value pair into the cache. /// /// If the key already exists, its value is updated and the weight is diff --git a/src/weighted_sharded.rs b/src/weighted_sharded.rs index d7679e7..ad5bbca 100644 --- a/src/weighted_sharded.rs +++ b/src/weighted_sharded.rs @@ -1,6 +1,5 @@ use std::borrow::Borrow; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use std::hash::{BuildHasher, Hash, Hasher, RandomState}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use crate::weighted::{Weigh, WeightedSieveCache}; @@ -15,19 +14,30 @@ const DEFAULT_SHARDS: usize = 16; /// the remainder given to the first N shards (mirroring the capacity /// distribution). /// +/// # Type Parameters +/// +/// * `K` - The key type, which must implement `Eq`, `Hash`, `Clone`, `Weigh`, `Send`, and `Sync` +/// * `V` - The value type, must implement `Weigh`, `Send`, and `Sync` +/// * `S` - The hash builder type for the underlying `HashMap` (default: `RandomState`). Must implement `BuildHasher` +/// /// # Weight overshoot /// /// Because each shard enforces independently, the worst-case total /// overshoot is `num_shards * max_single_entry_weight`. #[derive(Clone)] -pub struct WeightedShardedSieveCache +pub struct WeightedShardedSieveCache where K: Eq + Hash + Clone + Weigh + Send + Sync, V: Weigh + Send + Sync, + S: BuildHasher + Clone, { - shards: Vec>>>, + shards: Vec>>>, num_shards: usize, max_weight: usize, + + // We store the hasher here to increase the performance of methods accessing the hasher, + // by avoiding the need to lock a shard just to access the hasher. + hasher: S, } impl WeightedShardedSieveCache @@ -49,6 +59,51 @@ where capacity: usize, max_weight: usize, num_shards: usize, + ) -> Result { + Self::with_shards_and_hasher(capacity, max_weight, num_shards, Default::default()) + } +} + +impl WeightedShardedSieveCache +where + K: Eq + Hash + Clone + Weigh + Send + Sync, + V: Weigh + Send + Sync, + S: BuildHasher + Clone, +{ + /// Creates a new sharded weighted cache with the default number of shards (16) and a custom hash builder. + /// + /// # Arguments + /// + /// * `capacity` - The total capacity of the cache + /// * `max_weight` - The memory budget in bytes + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// Returns `Err` if `capacity` or `max_weight` is 0. + pub fn new_with_hasher( + capacity: usize, + max_weight: usize, + hasher: S, + ) -> Result { + Self::with_shards_and_hasher(capacity, max_weight, DEFAULT_SHARDS, hasher) + } + + /// Creates a new sharded weighted cache with a custom number of shards and hash builder. + /// + /// # Arguments + /// + /// * `capacity` - The total capacity of the cache + /// * `max_weight` - The memory budget in bytes + /// * `num_shards` - The number of shards to divide the cache into + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// Returns `Err` if `capacity`, `max_weight`, or `num_shards` is 0, + /// or if `max_weight < num_shards` (each shard needs at least 1 byte + /// of budget). + pub fn with_shards_and_hasher( + capacity: usize, + max_weight: usize, + num_shards: usize, + hasher: S, ) -> Result { if capacity == 0 { return Err("capacity must be greater than 0"); @@ -85,7 +140,8 @@ where }; // base_weight >= 1 is guaranteed by the max_weight >= num_shards check above - let cache = WeightedSieveCache::new(shard_capacity, shard_weight)?; + let cache = + WeightedSieveCache::new_with_hasher(shard_capacity, shard_weight, hasher.clone())?; shards.push(Arc::new(Mutex::new(cache))); } @@ -93,22 +149,42 @@ where shards, num_shards, max_weight, + hasher, }) } + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to another cache variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::WeightedShardedSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: WeightedShardedSieveCache = WeightedShardedSieveCache::with_shards_and_hasher(100, 1000, 4, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// ``` + pub fn hasher(&self) -> S { + self.hasher.clone() + } + #[inline] fn get_shard_index(&self, key: &Q) -> usize where Q: Hash + ?Sized, { - let mut hasher = DefaultHasher::new(); + let mut hasher = self.hasher.build_hasher(); key.hash(&mut hasher); let hash = hasher.finish() as usize; hash % self.num_shards } #[inline] - fn locked_shard(&self, key: &Q) -> MutexGuard<'_, WeightedSieveCache> + fn locked_shard(&self, key: &Q) -> MutexGuard<'_, WeightedSieveCache> where Q: Hash + ?Sized, { diff --git a/src/weighted_sync.rs b/src/weighted_sync.rs index ab547c1..35d9f38 100644 --- a/src/weighted_sync.rs +++ b/src/weighted_sync.rs @@ -1,5 +1,5 @@ use std::borrow::Borrow; -use std::hash::Hash; +use std::hash::{BuildHasher, Hash, RandomState}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use crate::weighted::{Weigh, WeightedSieveCache}; @@ -11,15 +11,22 @@ use crate::weighted::{Weigh, WeightedSieveCache}; /// thread safety. The weight bookkeeping lives inside the mutex alongside /// the cache data so that every mutation is atomic with respect to weight. /// +/// # Type Parameters +/// +/// * `K` - The key type, which must implement `Eq`, `Hash`, `Clone`, `Weigh`, `Send`, and `Sync` +/// * `V` - The value type, must implement `Weigh`, `Send`, and `Sync` +/// * `S` - The hash builder type for the underlying `HashMap` (default: `RandomState`). Must implement `BuildHasher` +/// /// For higher concurrency, see /// [`WeightedShardedSieveCache`](crate::WeightedShardedSieveCache). #[derive(Clone)] -pub struct WeightedSyncSieveCache +pub struct WeightedSyncSieveCache where K: Eq + Hash + Clone + Weigh + Send + Sync, V: Weigh + Send + Sync, + S: BuildHasher, { - inner: Arc>>, + inner: Arc>>, } impl WeightedSyncSieveCache @@ -31,14 +38,57 @@ where /// /// Returns `Err` if `capacity` or `max_weight` is 0. pub fn new(capacity: usize, max_weight: usize) -> Result { - let cache = WeightedSieveCache::new(capacity, max_weight)?; + Self::new_with_hasher(capacity, max_weight, Default::default()) + } +} + +impl WeightedSyncSieveCache +where + K: Eq + Hash + Clone + Weigh + Send + Sync, + V: Weigh + Send + Sync, + S: BuildHasher + Clone, +{ + /// Creates a new thread-safe weighted cache with a custom hash builder. + /// + /// # Arguments + /// + /// * `capacity` - The maximum number of entries in the cache + /// * `max_weight` - The memory budget in bytes + /// * `hasher` - A hash builder instance (e.g., from `ahash::AHasher` or `std::collections::hash_map::RandomState`) + /// + /// Returns `Err` if `capacity` or `max_weight` is 0. + pub fn new_with_hasher( + capacity: usize, + max_weight: usize, + hasher: S, + ) -> Result { + let cache = WeightedSieveCache::new_with_hasher(capacity, max_weight, hasher)?; Ok(Self { inner: Arc::new(Mutex::new(cache)), }) } + /// Returns a clone of the hash builder used by this cache. + /// + /// This is useful when converting to another cache variant and you want + /// to preserve the custom hasher configuration. + /// + /// # Examples + /// + /// ``` + /// # use sieve_cache::WeightedSyncSieveCache; + /// # use std::hash::BuildHasherDefault; + /// # use std::collections::hash_map::DefaultHasher; + /// let hasher = BuildHasherDefault::::default(); + /// let cache: WeightedSyncSieveCache = WeightedSyncSieveCache::new_with_hasher(100, 1000, hasher).unwrap(); + /// let retrieved_hasher = cache.hasher(); + /// ``` + pub fn hasher(&self) -> S { + self.locked_cache().hasher() + } + #[inline] - fn locked_cache(&self) -> MutexGuard<'_, WeightedSieveCache> { + fn locked_cache(&self) -> MutexGuard<'_, WeightedSieveCache> { self.inner.lock().unwrap_or_else(PoisonError::into_inner) } @@ -178,7 +228,7 @@ where /// Gets exclusive access to the underlying weighted cache. pub fn with_lock(&self, f: F) -> T where - F: FnOnce(&mut WeightedSieveCache) -> T, + F: FnOnce(&mut WeightedSieveCache) -> T, { let mut guard = self.locked_cache(); f(&mut guard) diff --git a/tests/custom_hasher_test.rs b/tests/custom_hasher_test.rs new file mode 100644 index 0000000..8df4f51 --- /dev/null +++ b/tests/custom_hasher_test.rs @@ -0,0 +1,232 @@ +mod custom_hasher_test { + use std::{ + cell::RefCell, + hash::{BuildHasher, Hasher}, + rc::Rc, + }; + + use sieve_cache::SieveCache; + #[cfg(feature = "sync")] + use sieve_cache::SyncSieveCache; + + #[derive(Clone)] + struct CustomHasher { + hash: u64, + invocations: Rc>, + } + + impl CustomHasher { + fn new() -> Self { + CustomHasher { + hash: 0, + invocations: Rc::new(RefCell::new(0)), + } + } + } + + impl Hasher for CustomHasher { + fn finish(&self) -> u64 { + self.hash + } + + fn write(&mut self, bytes: &[u8]) { + //convert the byte array to a u64. Just for testing. + //for u8, this is a single byte. + //The tests below should use a u8 key, as introducing keys longer than a single byte + //would cause issues with endianness and could lead to evictions. + for b in bytes { + self.hash = self.hash << 8 | (*b as u64); + } + + *self.invocations.borrow_mut() += 1; + } + } + + impl BuildHasher for CustomHasher { + type Hasher = Self; + + fn build_hasher(&self) -> Self::Hasher { + CustomHasher { + hash: 0, + invocations: Rc::clone(&self.invocations), + } + } + } + + #[test] + fn test_custom_hasher() { + let hasher = CustomHasher::new(); + let mut cache = + SieveCache::::new_with_hasher(10, hasher.clone()).unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(&100u64)); + assert_eq!(cache.get(&2), Some(&200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } + + #[cfg(feature = "sync")] + #[test] + fn test_custom_hasher_sync() { + let hasher = CustomHasher::new(); + let cache = + SyncSieveCache::::new_with_hasher(10, hasher.clone()).unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(100u64)); + assert_eq!(cache.get(&2), Some(200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } + + #[cfg(feature = "sharded")] + #[test] + fn test_custom_hasher_sharded() { + let hasher = CustomHasher::new(); + let cache = sieve_cache::ShardedSieveCache::::new_with_hasher( + 100, + hasher.clone(), + ) + .unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(100u64)); + assert_eq!(cache.get(&2), Some(200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } + + #[cfg(feature = "weighted")] + #[test] + fn test_custom_hasher_weighted() { + let hasher = CustomHasher::new(); + let mut cache = sieve_cache::WeightedSieveCache::::new_with_hasher( + 10, + 100, + hasher.clone(), + ) + .unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(&100u64)); + assert_eq!(cache.get(&2), Some(&200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } + + #[cfg(all(feature = "weighted", feature = "sync"))] + #[test] + fn test_custom_hasher_weighted_sync() { + let hasher = CustomHasher::new(); + let cache = sieve_cache::WeightedSyncSieveCache::::new_with_hasher( + 10, + 100, + hasher.clone(), + ) + .unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(100u64)); + assert_eq!(cache.get(&2), Some(200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } + + #[cfg(all(feature = "weighted", feature = "sharded"))] + #[test] + fn test_custom_hasher_weighted_sharded() { + let hasher = CustomHasher::new(); + let cache = + sieve_cache::WeightedShardedSieveCache::::new_with_hasher( + 10, + 100, + hasher.clone(), + ) + .unwrap(); + cache.insert(1, 100); + cache.insert(2, 200); + + assert_eq!(cache.get(&1), Some(100u64)); + assert_eq!(cache.get(&2), Some(200u64)); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(&3), None); + + cache.remove(&1); + cache.remove(&2); + + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), None); + assert_eq!(cache.len(), 0); + + //at least one invocation per get and remove, plus some for the insertions. + //The exact number depends on the implementation details of the sharding and eviction logic, + //as well as the hashing logic used in the standard library, which may call the hasher multiple times for certain operations. + assert!(*hasher.invocations.borrow() >= 9); + } +} diff --git a/tests/weighted_test.rs b/tests/weighted_test.rs index a06cd70..2236986 100644 --- a/tests/weighted_test.rs +++ b/tests/weighted_test.rs @@ -383,6 +383,38 @@ mod weighted_tests { assert!(evicted.is_some()); assert_eq!(cache.current_weight(), 0); } + + #[test] + fn custom_hasher_weighted_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let mut cache: WeightedSieveCache = + WeightedSieveCache::new_with_hasher(5, 1000, hasher).unwrap(); + + // Test insert and get with custom hasher + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get("key1"), Some(&"value1".to_string())); + assert_eq!(cache.get("key2"), Some(&"value2".to_string())); + + // Test weight tracking with custom hasher + let initial_weight = cache.current_weight(); + assert!(initial_weight > 0); + + // Test remove and weight decrease + cache.remove("key1"); + assert_eq!(cache.get("key1"), None); + let weight_after_remove = cache.current_weight(); + assert!(weight_after_remove < initial_weight); + + // Test contains_key + assert!(cache.contains_key("key2")); + assert!(!cache.contains_key("key1")); + } } #[cfg(all(feature = "weighted", feature = "sync"))] @@ -463,6 +495,35 @@ mod weighted_sync_tests { WeightedSyncSieveCache::new(10, 0); assert!(r.is_err()); } + + #[test] + fn custom_hasher_weighted_sync_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let cache: WeightedSyncSieveCache = + WeightedSyncSieveCache::new_with_hasher(5, 1000, hasher).unwrap(); + + // Test thread-safe insert and get with custom hasher + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); + assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string())); + + // Test weight tracking with custom hasher + assert!(cache.current_weight() > 0); + + // Test remove + cache.remove(&"key1".to_string()); + assert_eq!(cache.get(&"key1".to_string()), None); + + // Test contains_key + assert!(cache.contains_key(&"key2".to_string())); + assert!(!cache.contains_key(&"key1".to_string())); + } } #[cfg(all(feature = "weighted", feature = "sharded"))] @@ -607,4 +668,71 @@ mod weighted_sharded_tests { let (k, _) = pair.unwrap(); assert!(!cache.contains_key(&k)); } + + #[test] + fn custom_hasher_weighted_sharded_sievecache() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let cache: WeightedShardedSieveCache = + WeightedShardedSieveCache::new_with_hasher( + 10, + 1000, + BuildHasherDefault::::new(), + ) + .unwrap(); + + // Test insert and get with custom hasher + assert!(cache.insert("a".to_string(), 1)); + assert!(cache.insert("b".to_string(), 2)); + assert!(cache.insert("c".to_string(), 3)); + + // Verify values are retrievable + assert_eq!(cache.get(&"a".to_string()), Some(1)); + assert_eq!(cache.get(&"b".to_string()), Some(2)); + assert_eq!(cache.get(&"c".to_string()), Some(3)); + + // Test weight tracking + assert!(cache.current_weight() > 0); + + // Test remove + assert_eq!(cache.remove(&"a".to_string()), Some(1)); + assert_eq!(cache.get(&"a".to_string()), None); + + // Test contains_key + assert!(cache.contains_key(&"b".to_string())); + assert!(!cache.contains_key(&"a".to_string())); + } + + #[test] + fn custom_hasher_weighted_sharded_sievecache_with_shards() { + use std::collections::hash_map::DefaultHasher; + use std::hash::BuildHasherDefault; + + let hasher = BuildHasherDefault::::default(); + let cache: WeightedShardedSieveCache = + WeightedShardedSieveCache::with_shards_and_hasher(10, 1000, 4, hasher).unwrap(); + + // Test insert and get with custom hasher and custom shard count + assert!(cache.insert("key1".to_string(), "value1".to_string())); + assert!(cache.insert("key2".to_string(), "value2".to_string())); + assert!(cache.insert("key3".to_string(), "value3".to_string())); + assert!(cache.insert("key4".to_string(), "value4".to_string())); + + // Verify values are retrievable + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); + assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string())); + assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string())); + assert_eq!(cache.get(&"key4".to_string()), Some("value4".to_string())); + + // Test weight and operations + assert!(cache.current_weight() > 0); + + assert_eq!( + cache.remove(&"key2".to_string()), + Some("value2".to_string()) + ); + assert!(!cache.contains_key(&"key2".to_string())); + assert_eq!(cache.len(), 3); + } }