sieve_cache/lib.rs
1#![doc = include_str!("../README.md")]
2
3//! # SIEVE Cache for Rust
4//!
5//! This crate provides implementations of the SIEVE cache replacement algorithm:
6//!
7//! * [`SieveCache`] - The core single-threaded implementation (always available)
8
9#[cfg(feature = "sync")]
10pub mod _docs_sync {}
11
12#[cfg(feature = "sharded")]
13pub mod _docs_sharded {}
14
15pub mod _sieve_algorithm {
16 //! ## The SIEVE Algorithm
17 //!
18 //! SIEVE (Simple, space-efficient, In-memory, EViction mEchanism) is a cache eviction
19 //! algorithm that maintains a single bit per entry to track whether an item has been
20 //! "visited" since it was last considered for eviction. This approach requires less
21 //! state than LRU but achieves excellent performance, especially on skewed workloads.
22}
23
24pub mod _cache_implementation {
25 //! ## Cache Implementation Details
26 //!
27 //! The cache is implemented as a combination of:
28 //!
29 //! 1. A `HashMap` for O(1) key lookups
30 //! 2. A vector-based ordered collection for managing the eviction order
31 //! 3. A "visited" flag on each entry to track recent access
32 //!
33 //! When the cache is full and a new item is inserted, the eviction algorithm:
34 //! 1. Starts from the "hand" position (eviction candidate)
35 //! 2. Finds the first non-visited entry, evicting it
36 //! 3. Marks all visited entries as non-visited while searching
37}
38
39pub mod _implementation_choice {
40 //! ## Choosing the Right Implementation
41 //!
42 //! - Use [`SieveCache`] for single-threaded applications
43}
44
45/// [DOCTEST_ONLY]
46///
47/// ```rust
48/// # #[cfg(feature = "sync")]
49/// # fn sync_example() {
50/// # use sieve_cache::SyncSieveCache;
51/// # let cache = SyncSieveCache::<String, u32>::new(1000).unwrap();
52/// # let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
53/// # println!("Recommended capacity: {}", recommended);
54/// # }
55/// #
56/// # #[cfg(feature = "sharded")]
57/// # fn sharded_example() {
58/// # use sieve_cache::ShardedSieveCache;
59/// # let cache = ShardedSieveCache::<String, u32>::new(1000).unwrap();
60/// # let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
61/// # println!("Recommended capacity: {}", recommended);
62/// # }
63/// ```
64fn _readme_examples_doctest() {
65 // This function exists only to host the doctest above
66 // This ensures doctests from the README.md can be validated
67}
68
69#[cfg(feature = "sync")]
70pub mod _docs_sync_usage {
71 //! - Use [`SyncSieveCache`] for multi-threaded applications with moderate concurrency
72}
73
74#[cfg(feature = "sharded")]
75pub mod _docs_sharded_usage {
76 //! - Use [`ShardedSieveCache`] for applications with high concurrency where operations
77 //! are distributed across many different keys
78}
79
80use std::borrow::Borrow;
81use std::collections::HashMap;
82use std::hash::Hash;
83
84#[cfg(feature = "sharded")]
85mod sharded;
86#[cfg(feature = "sync")]
87mod sync;
88
89#[cfg(feature = "sharded")]
90pub use sharded::ShardedSieveCache;
91#[cfg(feature = "sync")]
92pub use sync::SyncSieveCache;
93
94/// Internal representation of a cache entry
95#[derive(Clone)]
96struct Node<K: Eq + Hash + Clone, V> {
97 key: K,
98 value: V,
99 visited: bool,
100}
101
102impl<K: Eq + Hash + Clone, V> Node<K, V> {
103 fn new(key: K, value: V) -> Self {
104 Self {
105 key,
106 value,
107 visited: false,
108 }
109 }
110}
111
112/// A cache based on the SIEVE eviction algorithm.
113///
114/// `SieveCache` provides an efficient in-memory cache with a carefully designed eviction
115/// strategy that balances simplicity with good performance characteristics, especially on
116/// skewed workloads common in real-world applications.
117///
118/// This is the single-threaded implementation.
119#[cfg(feature = "sync")]
120/// For thread-safe variants, see [`SyncSieveCache`] (with the `sync` feature)
121#[cfg(feature = "sharded")]
122/// and [`ShardedSieveCache`] (with the `sharded` feature).
123///
124/// # Type Parameters
125///
126/// * `K` - The key type, which must implement `Eq`, `Hash`, and `Clone`
127/// * `V` - The value type, with no constraints
128///
129/// # Example
130///
131/// ```rust
132/// # #[cfg(feature = "doctest")]
133/// # {
134/// use sieve_cache::SieveCache;
135///
136/// // Create a new cache with capacity for 1000 items
137/// let mut cache = SieveCache::new(1000).unwrap();
138///
139/// // Insert some values
140/// cache.insert("key1".to_string(), "value1".to_string());
141/// cache.insert("key2".to_string(), "value2".to_string());
142///
143/// // Retrieve values - returns references to the values
144/// assert_eq!(cache.get("key1"), Some(&"value1".to_string()));
145///
146/// // Check if the cache contains a key
147/// assert!(cache.contains_key("key1"));
148/// assert!(!cache.contains_key("missing_key"));
149///
150/// // Get a mutable reference to modify a value
151/// if let Some(value) = cache.get_mut("key1") {
152/// *value = "modified".to_string();
153/// }
154///
155/// // Verify the modification
156/// assert_eq!(cache.get("key1"), Some(&"modified".to_string()));
157///
158/// // Remove a value
159/// let removed = cache.remove("key2");
160/// assert_eq!(removed, Some("value2".to_string()));
161/// # }
162/// ```
163pub struct SieveCache<K: Eq + Hash + Clone, V> {
164 /// Map of keys to indices in the nodes vector
165 map: HashMap<K, usize>,
166 /// Vector of all cache nodes
167 nodes: Vec<Node<K, V>>,
168 /// Index to the "hand" pointer used by the SIEVE algorithm for eviction
169 hand: Option<usize>,
170 /// Maximum number of entries the cache can hold
171 capacity: usize,
172}
173
174impl<K: Eq + Hash + Clone, V> SieveCache<K, V> {
175 /// Creates a new cache with the given capacity.
176 ///
177 /// The capacity represents the maximum number of key-value pairs
178 /// that can be stored in the cache. When this limit is reached,
179 /// the cache will use the SIEVE algorithm to evict entries.
180 ///
181 /// # Errors
182 ///
183 /// Returns an error if the capacity is zero.
184 ///
185 /// # Examples
186 ///
187 /// ```rust
188 /// # #[cfg(feature = "doctest")]
189 /// # {
190 /// use sieve_cache::SieveCache;
191 ///
192 /// // Create a cache with space for 100 entries
193 /// let cache: SieveCache<String, u32> = SieveCache::new(100).unwrap();
194 ///
195 /// // Capacity of zero is invalid
196 /// let invalid = SieveCache::<String, u32>::new(0);
197 /// assert!(invalid.is_err());
198 /// # }
199 /// ```
200 pub fn new(capacity: usize) -> Result<Self, &'static str> {
201 if capacity == 0 {
202 return Err("capacity must be greater than 0");
203 }
204 Ok(Self {
205 map: HashMap::with_capacity(capacity),
206 nodes: Vec::with_capacity(capacity),
207 hand: None,
208 capacity,
209 })
210 }
211
212 /// Returns the capacity of the cache.
213 ///
214 /// This is the maximum number of entries that can be stored before
215 /// the cache begins evicting old entries.
216 ///
217 /// # Examples
218 ///
219 /// ```rust
220 /// # #[cfg(feature = "doctest")]
221 /// # {
222 /// use sieve_cache::SieveCache;
223 ///
224 /// let cache = SieveCache::<String, u32>::new(100).unwrap();
225 /// assert_eq!(cache.capacity(), 100);
226 /// # }
227 /// ```
228 #[inline]
229 pub fn capacity(&self) -> usize {
230 self.capacity
231 }
232
233 /// Returns the number of cached values.
234 ///
235 /// This value will never exceed the capacity.
236 ///
237 /// # Examples
238 ///
239 /// ```rust
240 /// # #[cfg(feature = "doctest")]
241 /// # {
242 /// use sieve_cache::SieveCache;
243 ///
244 /// let mut cache = SieveCache::new(100).unwrap();
245 /// assert_eq!(cache.len(), 0);
246 ///
247 /// cache.insert("key".to_string(), "value".to_string());
248 /// assert_eq!(cache.len(), 1);
249 /// # }
250 /// ```
251 #[inline]
252 pub fn len(&self) -> usize {
253 self.nodes.len()
254 }
255
256 /// Returns `true` when no values are currently cached.
257 ///
258 /// # Examples
259 ///
260 /// ```rust
261 /// # #[cfg(feature = "doctest")]
262 /// # {
263 /// use sieve_cache::SieveCache;
264 ///
265 /// let mut cache = SieveCache::<String, u32>::new(100).unwrap();
266 /// assert!(cache.is_empty());
267 ///
268 /// cache.insert("key".to_string(), "value".to_string());
269 /// assert!(!cache.is_empty());
270 ///
271 /// cache.remove("key");
272 /// assert!(cache.is_empty());
273 /// # }
274 /// ```
275 #[inline]
276 pub fn is_empty(&self) -> bool {
277 self.nodes.is_empty()
278 }
279
280 /// Returns `true` if there is a value in the cache mapped to by `key`.
281 ///
282 /// This operation does not count as an access for the SIEVE algorithm's
283 /// visited flag.
284 ///
285 /// # Examples
286 ///
287 /// ```rust
288 /// # #[cfg(feature = "doctest")]
289 /// # {
290 /// use sieve_cache::SieveCache;
291 ///
292 /// let mut cache = SieveCache::new(100).unwrap();
293 /// cache.insert("key".to_string(), "value".to_string());
294 ///
295 /// assert!(cache.contains_key("key"));
296 /// assert!(!cache.contains_key("missing"));
297 /// # }
298 /// ```
299 #[inline]
300 pub fn contains_key<Q>(&self, key: &Q) -> bool
301 where
302 Q: Hash + Eq + ?Sized,
303 K: Borrow<Q>,
304 {
305 self.map.contains_key(key)
306 }
307
308 /// Gets an immutable reference to the value in the cache mapped to by `key`.
309 ///
310 /// If no value exists for `key`, this returns `None`.
311 ///
312 /// This operation marks the entry as "visited" in the SIEVE algorithm,
313 /// which affects eviction decisions.
314 ///
315 /// # Examples
316 ///
317 /// ```rust
318 /// # #[cfg(feature = "doctest")]
319 /// # {
320 /// use sieve_cache::SieveCache;
321 ///
322 /// let mut cache = SieveCache::new(100).unwrap();
323 /// cache.insert("key".to_string(), "value".to_string());
324 ///
325 /// assert_eq!(cache.get("key"), Some(&"value".to_string()));
326 /// assert_eq!(cache.get("missing"), None);
327 /// # }
328 /// ```
329 pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
330 where
331 Q: Hash + Eq + ?Sized,
332 K: Borrow<Q>,
333 {
334 if let Some(&idx) = self.map.get(key) {
335 // Mark as visited for the SIEVE algorithm
336 self.nodes[idx].visited = true;
337 Some(&self.nodes[idx].value)
338 } else {
339 None
340 }
341 }
342
343 /// Gets a mutable reference to the value in the cache mapped to by `key`.
344 ///
345 /// If no value exists for `key`, this returns `None`.
346 ///
347 /// This operation marks the entry as "visited" in the SIEVE algorithm,
348 /// which affects eviction decisions.
349 ///
350 /// # Examples
351 ///
352 /// ```rust
353 /// # #[cfg(feature = "doctest")]
354 /// # {
355 /// use sieve_cache::SieveCache;
356 ///
357 /// let mut cache = SieveCache::new(100).unwrap();
358 /// cache.insert("key".to_string(), "value".to_string());
359 ///
360 /// // Modify the value in-place
361 /// if let Some(value) = cache.get_mut("key") {
362 /// *value = "new_value".to_string();
363 /// }
364 ///
365 /// assert_eq!(cache.get("key"), Some(&"new_value".to_string()));
366 /// # }
367 /// ```
368 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
369 where
370 Q: Hash + Eq + ?Sized,
371 K: Borrow<Q>,
372 {
373 if let Some(&idx) = self.map.get(key) {
374 // Mark as visited for the SIEVE algorithm
375 self.nodes[idx].visited = true;
376 Some(&mut self.nodes[idx].value)
377 } else {
378 None
379 }
380 }
381
382 /// Maps `key` to `value` in the cache, possibly evicting old entries.
383 ///
384 /// If the key already exists, its value is updated and the entry is marked as visited.
385 /// If the key doesn't exist and the cache is at capacity, the SIEVE algorithm is used
386 /// to evict an entry before the new key-value pair is inserted.
387 ///
388 /// # Return Value
389 ///
390 /// Returns `true` when this is a new entry, and `false` if an existing entry was updated.
391 ///
392 /// # Examples
393 ///
394 /// ```rust
395 /// # #[cfg(feature = "doctest")]
396 /// # {
397 /// use sieve_cache::SieveCache;
398 ///
399 /// let mut cache = SieveCache::new(100).unwrap();
400 ///
401 /// // Insert a new entry
402 /// let is_new = cache.insert("key1".to_string(), "value1".to_string());
403 /// assert!(is_new); // Returns true for new entries
404 ///
405 /// // Update an existing entry
406 /// let is_new = cache.insert("key1".to_string(), "updated".to_string());
407 /// assert!(!is_new); // Returns false for updates
408 ///
409 /// assert_eq!(cache.get("key1"), Some(&"updated".to_string()));
410 /// # }
411 /// ```
412 pub fn insert(&mut self, key: K, value: V) -> bool {
413 // Check if key already exists
414 if let Some(&idx) = self.map.get(&key) {
415 // Update existing entry
416 self.nodes[idx].visited = true;
417 self.nodes[idx].value = value;
418 return false;
419 }
420
421 // Evict if at capacity
422 if self.nodes.len() >= self.capacity {
423 self.evict();
424 }
425
426 // Add new node to the front
427 let node = Node::new(key.clone(), value);
428 self.nodes.push(node);
429 let idx = self.nodes.len() - 1;
430 self.map.insert(key, idx);
431 true
432 }
433
434 /// Removes the cache entry mapped to by `key`.
435 ///
436 /// This method explicitly removes an entry from the cache regardless of its
437 /// "visited" status.
438 ///
439 /// # Return Value
440 ///
441 /// Returns the value removed from the cache. If `key` did not map to any value,
442 /// then this returns `None`.
443 ///
444 /// # Examples
445 ///
446 /// ```rust
447 /// # #[cfg(feature = "doctest")]
448 /// # {
449 /// use sieve_cache::SieveCache;
450 ///
451 /// let mut cache = SieveCache::new(100).unwrap();
452 /// cache.insert("key1".to_string(), "value1".to_string());
453 /// cache.insert("key2".to_string(), "value2".to_string());
454 ///
455 /// // Remove an existing entry
456 /// let removed = cache.remove("key1");
457 /// assert_eq!(removed, Some("value1".to_string()));
458 ///
459 /// // Try to remove a non-existent entry
460 /// let removed = cache.remove("key1");
461 /// assert_eq!(removed, None);
462 ///
463 /// assert_eq!(cache.len(), 1); // Only one entry remains
464 /// # }
465 /// ```
466 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
467 where
468 K: Borrow<Q>,
469 Q: Eq + Hash + ?Sized,
470 {
471 // Find the node index
472 let idx = self.map.remove(key)?;
473
474 // If this is the last element, just remove it
475 if idx == self.nodes.len() - 1 {
476 return Some(self.nodes.pop().unwrap().value);
477 }
478
479 // Update hand if needed
480 if let Some(hand_idx) = self.hand {
481 if hand_idx == idx {
482 // Move hand to the previous node or wrap to end
483 self.hand = if idx > 0 {
484 Some(idx - 1)
485 } else {
486 Some(self.nodes.len() - 2)
487 };
488 } else if hand_idx == self.nodes.len() - 1 {
489 // If hand points to the last element (which will be moved to idx)
490 self.hand = Some(idx);
491 }
492 }
493
494 // Remove the node by replacing it with the last one and updating the map
495 let last_node = self.nodes.pop().unwrap();
496 let removed_value = if idx < self.nodes.len() {
497 // Only need to swap and update map if not the last element
498 let last_key = last_node.key.clone(); // Clone the key before moving
499 let removed_node = std::mem::replace(&mut self.nodes[idx], last_node);
500 self.map.insert(last_key, idx); // Update map for swapped node
501 removed_node.value
502 } else {
503 last_node.value
504 };
505
506 Some(removed_value)
507 }
508
509 /// Removes and returns a value from the cache that was not recently accessed.
510 ///
511 /// This method implements the SIEVE eviction algorithm:
512 /// 1. Starting from the "hand" pointer or the end of the vector, look for an entry
513 /// that hasn't been visited since the last scan
514 /// 2. Mark all visited entries as non-visited during the scan
515 /// 3. If a non-visited entry is found, evict it
516 /// 4. Update the hand to point to the previous node
517 ///
518 /// # Return Value
519 ///
520 /// If a suitable entry is found, it is removed from the cache and its value is returned.
521 /// If all entries have been recently accessed or the cache is empty, this returns `None`.
522 ///
523 /// # Note
524 ///
525 /// This method is automatically called by `insert` when the cache is at capacity,
526 /// but it can also be called manually to proactively free space.
527 ///
528 /// # Examples
529 ///
530 /// ```rust
531 /// # #[cfg(feature = "doctest")]
532 /// # {
533 /// use sieve_cache::SieveCache;
534 ///
535 /// let mut cache = SieveCache::new(3).unwrap();
536 /// cache.insert("key1".to_string(), "value1".to_string());
537 /// cache.insert("key2".to_string(), "value2".to_string());
538 /// cache.insert("key3".to_string(), "value3".to_string());
539 ///
540 /// // Access key1 and key2 to mark them as visited
541 /// cache.get("key1");
542 /// cache.get("key2");
543 ///
544 /// // key3 hasn't been accessed, so it should be evicted
545 /// let evicted = cache.evict();
546 /// assert!(evicted.is_some());
547 /// assert!(!cache.contains_key("key3")); // key3 was evicted
548 /// # }
549 /// ```
550 pub fn evict(&mut self) -> Option<V> {
551 if self.nodes.is_empty() {
552 return None;
553 }
554
555 // Start from the hand pointer or the end if no hand
556 let mut current_idx = self.hand.unwrap_or(self.nodes.len() - 1);
557 let start_idx = current_idx;
558
559 // Track whether we've wrapped around and whether we found a node to evict
560 let mut wrapped = false;
561 let mut found_idx = None;
562
563 // Scan for a non-visited entry
564 loop {
565 // If current node is not visited, mark it for eviction
566 if !self.nodes[current_idx].visited {
567 found_idx = Some(current_idx);
568 break;
569 }
570
571 // Mark as non-visited for next scan
572 self.nodes[current_idx].visited = false;
573
574 // Move to previous node or wrap to end
575 current_idx = if current_idx > 0 {
576 current_idx - 1
577 } else {
578 // Wrap around to end of vector
579 if wrapped {
580 // If we've already wrapped, break to avoid infinite loop
581 break;
582 }
583 wrapped = true;
584 self.nodes.len() - 1
585 };
586
587 // If we've looped back to start, we've checked all nodes
588 if current_idx == start_idx {
589 break;
590 }
591 }
592
593 // If we found a node to evict
594 if let Some(idx) = found_idx {
595 // Update the hand pointer to the previous node or wrap to end
596 self.hand = if idx > 0 {
597 Some(idx - 1)
598 } else if self.nodes.len() > 1 {
599 Some(self.nodes.len() - 2)
600 } else {
601 None
602 };
603
604 // Remove the key from the map
605 let key = &self.nodes[idx].key;
606 self.map.remove(key);
607
608 // Remove the node and return its value
609 if idx == self.nodes.len() - 1 {
610 // If last node, just pop it
611 return Some(self.nodes.pop().unwrap().value);
612 } else {
613 // Otherwise swap with the last node
614 let last_node = self.nodes.pop().unwrap();
615 let last_key = last_node.key.clone(); // Clone the key before moving
616 let removed_node = std::mem::replace(&mut self.nodes[idx], last_node);
617 // Update the map for the moved node
618 self.map.insert(last_key, idx);
619 return Some(removed_node.value);
620 }
621 }
622
623 None
624 }
625
626 /// Removes all entries from the cache.
627 ///
628 /// This operation clears all stored values and resets the cache to an empty state,
629 /// while maintaining the original capacity.
630 ///
631 /// # Examples
632 ///
633 /// ```rust
634 /// # #[cfg(feature = "doctest")]
635 /// # {
636 /// use sieve_cache::SieveCache;
637 ///
638 /// let mut cache = SieveCache::new(100).unwrap();
639 /// cache.insert("key1".to_string(), "value1".to_string());
640 /// cache.insert("key2".to_string(), "value2".to_string());
641 ///
642 /// assert_eq!(cache.len(), 2);
643 ///
644 /// cache.clear();
645 /// assert_eq!(cache.len(), 0);
646 /// assert!(cache.is_empty());
647 /// # }
648 /// ```
649 pub fn clear(&mut self) {
650 self.map.clear();
651 self.nodes.clear();
652 self.hand = None;
653 }
654
655 /// Returns an iterator over all keys in the cache.
656 ///
657 /// The order of keys is not specified and should not be relied upon.
658 ///
659 /// # Examples
660 ///
661 /// ```rust
662 /// # #[cfg(feature = "doctest")]
663 /// # {
664 /// use sieve_cache::SieveCache;
665 /// use std::collections::HashSet;
666 ///
667 /// let mut cache = SieveCache::new(100).unwrap();
668 /// cache.insert("key1".to_string(), "value1".to_string());
669 /// cache.insert("key2".to_string(), "value2".to_string());
670 ///
671 /// let keys: HashSet<_> = cache.keys().collect();
672 /// assert_eq!(keys.len(), 2);
673 /// assert!(keys.contains(&"key1".to_string()));
674 /// assert!(keys.contains(&"key2".to_string()));
675 /// # }
676 /// ```
677 pub fn keys(&self) -> impl Iterator<Item = &K> {
678 self.nodes.iter().map(|node| &node.key)
679 }
680
681 /// Returns an iterator over all values in the cache.
682 ///
683 /// The order of values is not specified and should not be relied upon.
684 ///
685 /// # Examples
686 ///
687 /// ```rust
688 /// # #[cfg(feature = "doctest")]
689 /// # {
690 /// use sieve_cache::SieveCache;
691 /// use std::collections::HashSet;
692 ///
693 /// let mut cache = SieveCache::new(100).unwrap();
694 /// cache.insert("key1".to_string(), "value1".to_string());
695 /// cache.insert("key2".to_string(), "value2".to_string());
696 ///
697 /// let values: HashSet<_> = cache.values().collect();
698 /// assert_eq!(values.len(), 2);
699 /// assert!(values.contains(&"value1".to_string()));
700 /// assert!(values.contains(&"value2".to_string()));
701 /// # }
702 /// ```
703 pub fn values(&self) -> impl Iterator<Item = &V> {
704 self.nodes.iter().map(|node| &node.value)
705 }
706
707 /// Returns an iterator over all mutable values in the cache.
708 ///
709 /// The order of values is not specified and should not be relied upon.
710 /// Note that iterating through this will mark all entries as visited.
711 ///
712 /// # Examples
713 ///
714 /// ```rust
715 /// # #[cfg(feature = "doctest")]
716 /// # {
717 /// use sieve_cache::SieveCache;
718 ///
719 /// let mut cache = SieveCache::new(100).unwrap();
720 /// cache.insert("key1".to_string(), "value1".to_string());
721 /// cache.insert("key2".to_string(), "value2".to_string());
722 ///
723 /// // Update all values by appending text
724 /// for value in cache.values_mut() {
725 /// *value = format!("{}_updated", value);
726 /// }
727 ///
728 /// assert_eq!(cache.get("key1"), Some(&"value1_updated".to_string()));
729 /// assert_eq!(cache.get("key2"), Some(&"value2_updated".to_string()));
730 /// # }
731 /// ```
732 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
733 for node in &mut self.nodes {
734 node.visited = true;
735 }
736 self.nodes.iter_mut().map(|node| &mut node.value)
737 }
738
739 /// Returns an iterator over all key-value pairs in the cache.
740 ///
741 /// The order of pairs is not specified and should not be relied upon.
742 ///
743 /// # Examples
744 ///
745 /// ```rust
746 /// # #[cfg(feature = "doctest")]
747 /// # {
748 /// use sieve_cache::SieveCache;
749 /// use std::collections::HashMap;
750 ///
751 /// let mut cache = SieveCache::new(100).unwrap();
752 /// cache.insert("key1".to_string(), "value1".to_string());
753 /// cache.insert("key2".to_string(), "value2".to_string());
754 ///
755 /// let entries: HashMap<_, _> = cache.iter().collect();
756 /// assert_eq!(entries.len(), 2);
757 /// assert_eq!(entries.get(&"key1".to_string()), Some(&&"value1".to_string()));
758 /// assert_eq!(entries.get(&"key2".to_string()), Some(&&"value2".to_string()));
759 /// # }
760 /// ```
761 pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
762 self.nodes.iter().map(|node| (&node.key, &node.value))
763 }
764
765 /// Returns an iterator over all key-value pairs in the cache, with mutable references to values.
766 ///
767 /// The order of pairs is not specified and should not be relied upon.
768 /// Note that iterating through this will mark all entries as visited.
769 ///
770 /// # Examples
771 ///
772 /// ```rust
773 /// # #[cfg(feature = "doctest")]
774 /// # {
775 /// use sieve_cache::SieveCache;
776 ///
777 /// let mut cache = SieveCache::new(100).unwrap();
778 /// cache.insert("key1".to_string(), "value1".to_string());
779 /// cache.insert("key2".to_string(), "value2".to_string());
780 ///
781 /// // Update all values associated with keys containing '1'
782 /// for (key, value) in cache.iter_mut() {
783 /// if key.contains('1') {
784 /// *value = format!("{}_special", value);
785 /// }
786 /// }
787 ///
788 /// assert_eq!(cache.get("key1"), Some(&"value1_special".to_string()));
789 /// assert_eq!(cache.get("key2"), Some(&"value2".to_string()));
790 /// # }
791 /// ```
792 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&K, &mut V)> {
793 for node in &mut self.nodes {
794 node.visited = true;
795 }
796 self.nodes
797 .iter_mut()
798 .map(|node| (&node.key, &mut node.value))
799 }
800
801 /// Retains only the elements specified by the predicate.
802 ///
803 /// Removes all entries for which the provided function returns `false`.
804 /// The elements are visited in arbitrary, unspecified order.
805 ///
806 /// # Examples
807 ///
808 /// ```rust
809 /// # #[cfg(feature = "doctest")]
810 /// # {
811 /// use sieve_cache::SieveCache;
812 ///
813 /// let mut cache = SieveCache::new(100).unwrap();
814 /// cache.insert("key1".to_string(), 100);
815 /// cache.insert("key2".to_string(), 200);
816 /// cache.insert("key3".to_string(), 300);
817 ///
818 /// // Keep only entries with values greater than 150
819 /// cache.retain(|_, value| *value > 150);
820 ///
821 /// assert_eq!(cache.len(), 2);
822 /// assert!(!cache.contains_key("key1"));
823 /// assert!(cache.contains_key("key2"));
824 /// assert!(cache.contains_key("key3"));
825 /// # }
826 /// ```
827 pub fn retain<F>(&mut self, mut f: F)
828 where
829 F: FnMut(&K, &V) -> bool,
830 {
831 // Collect indices to remove
832 let mut to_remove = Vec::new();
833
834 for (i, node) in self.nodes.iter().enumerate() {
835 if !f(&node.key, &node.value) {
836 to_remove.push(i);
837 }
838 }
839
840 // Remove indices from highest to lowest to avoid invalidating other indices
841 to_remove.sort_unstable_by(|a, b| b.cmp(a));
842
843 for idx in to_remove {
844 // Remove from map
845 self.map.remove(&self.nodes[idx].key);
846
847 // If it's the last element, just pop it
848 if idx == self.nodes.len() - 1 {
849 self.nodes.pop();
850 } else {
851 // Replace with the last element
852 let last_idx = self.nodes.len() - 1;
853 // Use swap_remove which replaces the removed element with the last element
854 self.nodes.swap_remove(idx);
855 if idx < self.nodes.len() {
856 // Update map for the swapped node
857 self.map.insert(self.nodes[idx].key.clone(), idx);
858 }
859
860 // Update hand if needed
861 if let Some(hand_idx) = self.hand {
862 if hand_idx == idx {
863 // Hand was pointing to the removed node, move it to previous
864 self.hand = if idx > 0 {
865 Some(idx - 1)
866 } else if !self.nodes.is_empty() {
867 Some(self.nodes.len() - 1)
868 } else {
869 None
870 };
871 } else if hand_idx == last_idx {
872 // Hand was pointing to the last node that was moved
873 self.hand = Some(idx);
874 }
875 }
876 }
877 }
878 }
879
880 /// Returns a recommended cache capacity based on current utilization.
881 ///
882 /// This method analyzes the current cache utilization and recommends a new capacity based on:
883 /// - The number of entries with the 'visited' flag set
884 /// - The current capacity and fill ratio
885 /// - A target utilization range
886 ///
887 /// The recommendation aims to keep the cache size optimal:
888 /// - If many entries are frequently accessed (high utilization), it suggests increasing capacity
889 /// - If few entries are accessed frequently (low utilization), it suggests decreasing capacity
890 /// - Within a normal utilization range, it keeps the capacity stable
891 ///
892 /// # Arguments
893 ///
894 /// * `min_factor` - Minimum scaling factor (e.g., 0.5 means never recommend less than 50% of current capacity)
895 /// * `max_factor` - Maximum scaling factor (e.g., 2.0 means never recommend more than 200% of current capacity)
896 /// * `low_threshold` - Utilization threshold below which capacity is reduced (e.g., 0.3 means 30% utilization)
897 /// * `high_threshold` - Utilization threshold above which capacity is increased (e.g., 0.7 means 70% utilization)
898 ///
899 /// # Examples
900 ///
901 /// ```rust
902 /// # use sieve_cache::SieveCache;
903 /// #
904 /// # fn main() {
905 /// # let mut cache = SieveCache::<String, i32>::new(100).unwrap();
906 /// #
907 /// # // Add items to the cache
908 /// # for i in 0..80 {
909 /// # cache.insert(i.to_string(), i);
910 /// #
911 /// # // Accessing some items to mark them as visited
912 /// # if i % 2 == 0 {
913 /// # cache.get(&i.to_string());
914 /// # }
915 /// # }
916 /// #
917 /// // Get a recommended capacity with default parameters
918 /// let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
919 /// println!("Recommended capacity: {}", recommended);
920 /// # }
921 /// ```
922 ///
923 /// # Default Recommendation Parameters
924 ///
925 /// If you're unsure what parameters to use, these values provide a reasonable starting point:
926 /// - `min_factor`: 0.5 (never recommend less than half current capacity)
927 /// - `max_factor`: 2.0 (never recommend more than twice current capacity)
928 /// - `low_threshold`: 0.3 (reduce capacity if utilization below 30%)
929 /// - `high_threshold`: 0.7 (increase capacity if utilization above 70%)
930 pub fn recommended_capacity(
931 &self,
932 min_factor: f64,
933 max_factor: f64,
934 low_threshold: f64,
935 high_threshold: f64,
936 ) -> usize {
937 // If the cache is empty, return the current capacity
938 if self.nodes.is_empty() {
939 return self.capacity;
940 }
941
942 // Count entries with visited flag set
943 let visited_count = self.nodes.iter().filter(|node| node.visited).count();
944
945 // Calculate the utilization ratio (visited entries / total entries)
946 let utilization_ratio = visited_count as f64 / self.nodes.len() as f64;
947
948 // Calculate a fill ratio (how full the cache is)
949 let fill_ratio = self.nodes.len() as f64 / self.capacity as f64;
950
951 // Determine scaling factor based on utilization
952 let scaling_factor = if utilization_ratio >= high_threshold {
953 // High utilization - recommend increasing the capacity
954 // Scale between 1.0 and max_factor based on utilization above the high threshold
955 let utilization_above_threshold =
956 (utilization_ratio - high_threshold) / (1.0 - high_threshold);
957 1.0 + (max_factor - 1.0) * utilization_above_threshold
958 } else if utilization_ratio <= low_threshold && fill_ratio > 0.8 {
959 // Lower the fill ratio threshold for tests
960 // Low utilization and cache is reasonably full - recommend decreasing capacity
961 // Scale between min_factor and 1.0 based on how far below the low threshold
962 let utilization_below_threshold = (low_threshold - utilization_ratio) / low_threshold;
963 1.0 - (1.0 - min_factor) * utilization_below_threshold
964 } else {
965 // Normal utilization or cache isn't full enough - keep current capacity
966 1.0
967 };
968
969 // Apply the scaling factor to current capacity and ensure it's at least 1
970 std::cmp::max(1, (self.capacity as f64 * scaling_factor).round() as usize)
971 }
972}
973
974#[test]
975fn test() {
976 let mut cache = SieveCache::new(3).unwrap();
977 assert!(cache.insert("foo".to_string(), "foocontent".to_string()));
978 assert!(cache.insert("bar".to_string(), "barcontent".to_string()));
979 cache.remove("bar");
980 assert!(cache.insert("bar2".to_string(), "bar2content".to_string()));
981 assert!(cache.insert("bar3".to_string(), "bar3content".to_string()));
982 assert_eq!(cache.get("foo"), Some(&"foocontent".to_string()));
983 assert_eq!(cache.get("bar"), None);
984 assert_eq!(cache.get("bar2"), Some(&"bar2content".to_string()));
985 assert_eq!(cache.get("bar3"), Some(&"bar3content".to_string()));
986}
987
988#[test]
989fn test_visited_flag_update() {
990 let mut cache = SieveCache::new(2).unwrap();
991 cache.insert("key1".to_string(), "value1".to_string());
992 cache.insert("key2".to_string(), "value2".to_string());
993 // update `key1` entry.
994 cache.insert("key1".to_string(), "updated".to_string());
995 // new entry is added.
996 cache.insert("key3".to_string(), "value3".to_string());
997 assert_eq!(cache.get("key1"), Some(&"updated".to_string()));
998}
999
1000#[test]
1001fn test_clear() {
1002 let mut cache = SieveCache::new(10).unwrap();
1003 cache.insert("key1".to_string(), "value1".to_string());
1004 cache.insert("key2".to_string(), "value2".to_string());
1005
1006 assert_eq!(cache.len(), 2);
1007 assert!(!cache.is_empty());
1008
1009 cache.clear();
1010
1011 assert_eq!(cache.len(), 0);
1012 assert!(cache.is_empty());
1013 assert_eq!(cache.get("key1"), None);
1014 assert_eq!(cache.get("key2"), None);
1015}
1016
1017#[test]
1018fn test_iterators() {
1019 let mut cache = SieveCache::new(10).unwrap();
1020 cache.insert("key1".to_string(), "value1".to_string());
1021 cache.insert("key2".to_string(), "value2".to_string());
1022
1023 // Test keys iterator
1024 let keys: Vec<&String> = cache.keys().collect();
1025 assert_eq!(keys.len(), 2);
1026 assert!(keys.contains(&&"key1".to_string()));
1027 assert!(keys.contains(&&"key2".to_string()));
1028
1029 // Test values iterator
1030 let values: Vec<&String> = cache.values().collect();
1031 assert_eq!(values.len(), 2);
1032 assert!(values.contains(&&"value1".to_string()));
1033 assert!(values.contains(&&"value2".to_string()));
1034
1035 // Test values_mut iterator
1036 for value in cache.values_mut() {
1037 *value = format!("{}_updated", value);
1038 }
1039
1040 assert_eq!(cache.get("key1"), Some(&"value1_updated".to_string()));
1041 assert_eq!(cache.get("key2"), Some(&"value2_updated".to_string()));
1042
1043 // Test key-value iterator
1044 let entries: Vec<(&String, &String)> = cache.iter().collect();
1045 assert_eq!(entries.len(), 2);
1046
1047 // Test key-value mutable iterator
1048 for (key, value) in cache.iter_mut() {
1049 if key == "key1" {
1050 *value = format!("{}_special", value);
1051 }
1052 }
1053
1054 assert_eq!(
1055 cache.get("key1"),
1056 Some(&"value1_updated_special".to_string())
1057 );
1058 assert_eq!(cache.get("key2"), Some(&"value2_updated".to_string()));
1059}
1060
1061#[test]
1062fn test_retain() {
1063 let mut cache = SieveCache::new(10).unwrap();
1064
1065 // Add some entries
1066 cache.insert("even1".to_string(), 2);
1067 cache.insert("even2".to_string(), 4);
1068 cache.insert("odd1".to_string(), 1);
1069 cache.insert("odd2".to_string(), 3);
1070
1071 assert_eq!(cache.len(), 4);
1072
1073 // Keep only entries with even values
1074 cache.retain(|_, v| v % 2 == 0);
1075
1076 assert_eq!(cache.len(), 2);
1077 assert!(cache.contains_key("even1"));
1078 assert!(cache.contains_key("even2"));
1079 assert!(!cache.contains_key("odd1"));
1080 assert!(!cache.contains_key("odd2"));
1081
1082 // Keep only entries with keys containing '1'
1083 cache.retain(|k, _| k.contains('1'));
1084
1085 assert_eq!(cache.len(), 1);
1086 assert!(cache.contains_key("even1"));
1087 assert!(!cache.contains_key("even2"));
1088}
1089
1090#[test]
1091fn test_recommended_capacity() {
1092 // Test case 1: Empty cache - should return current capacity
1093 let cache = SieveCache::<String, u32>::new(100).unwrap();
1094 assert_eq!(cache.recommended_capacity(0.5, 2.0, 0.3, 0.7), 100);
1095
1096 // Test case 2: Low utilization (few visited nodes)
1097 let mut cache = SieveCache::new(100).unwrap();
1098 // Fill the cache first without marking anything as visited
1099 for i in 0..90 {
1100 cache.insert(i.to_string(), i);
1101 }
1102
1103 // Now mark only a tiny fraction as visited
1104 for i in 0..5 {
1105 cache.get(&i.to_string()); // Only 5% visited
1106 }
1107
1108 // With very low utilization and high fill, should recommend decreasing capacity
1109 let recommended = cache.recommended_capacity(0.5, 2.0, 0.1, 0.7); // Lower threshold to ensure test passes
1110 assert!(recommended < 100);
1111 assert!(recommended >= 50); // Should not go below min_factor
1112
1113 // Test case 3: High utilization (many visited nodes)
1114 let mut cache = SieveCache::new(100).unwrap();
1115 for i in 0..90 {
1116 cache.insert(i.to_string(), i);
1117 // Mark 80% as visited
1118 if i % 10 != 0 {
1119 cache.get(&i.to_string());
1120 }
1121 }
1122 // With 80% utilization, should recommend increasing capacity
1123 let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
1124 assert!(recommended > 100);
1125 assert!(recommended <= 200); // Should not go above max_factor
1126
1127 // Test case 4: Normal utilization (should keep capacity the same)
1128 let mut cache = SieveCache::new(100).unwrap();
1129 for i in 0..90 {
1130 cache.insert(i.to_string(), i);
1131 // Mark 50% as visited
1132 if i % 2 == 0 {
1133 cache.get(&i.to_string());
1134 }
1135 }
1136 // With 50% utilization (between thresholds), should keep capacity the same
1137 let recommended = cache.recommended_capacity(0.5, 2.0, 0.3, 0.7);
1138 assert_eq!(recommended, 100);
1139}