Skip to content

Commit b327d82

Browse files
committed
Enhance S3 FIFO cache safety and documentation
- Improved thread safety documentation for `S3FifoCache` by clarifying the ownership and synchronization guarantees of `NonNull<Node<K, V>>`. - Updated panic conditions in the `with_ratios` method to include checks for `small_ratio` and `ghost_ratio`, ensuring robust input validation. - Refactored eviction logic to avoid unnecessary key cloning during Small queue evictions, enhancing performance and memory efficiency. - Enhanced overall documentation for clarity and correctness, aligning with Rust API Guidelines.
1 parent 7bd52d6 commit b327d82

1 file changed

Lines changed: 29 additions & 17 deletions

File tree

src/policy/s3_fifo.rs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -525,15 +525,24 @@ pub struct S3FifoCache<K, V> {
525525
metrics: S3FifoMetrics,
526526
}
527527

528-
// SAFETY: S3FifoCache can be sent between threads if K and V are Send.
528+
// SAFETY: `NonNull<Node<K, V>>` is `!Send`, but the pointers exclusively own
529+
// their heap allocations and are never shared across threads without external
530+
// synchronisation. Sending the cache moves full ownership of all nodes, which
531+
// is safe when K and V themselves are `Send`.
529532
unsafe impl<K, V> Send for S3FifoCache<K, V>
530533
where
531534
K: Clone + Eq + Hash + Send,
532535
V: Send,
533536
{
534537
}
535538

536-
// SAFETY: S3FifoCache can be shared between threads if K and V are Sync.
539+
// SAFETY: `NonNull<Node<K, V>>` is `!Sync`, but all `&self` methods on
540+
// `S3FifoCache` only perform read-only access through these pointers
541+
// (`contains`, `peek`, `len`, iterators). The sole interior-mutation path is
542+
// `get_shared` (`pub(crate)`), which bumps `Node::freq` via `AtomicU8` —
543+
// an inherently `Sync` type — so concurrent `&self` access is data-race-free.
544+
// Structural mutations (insert/remove/evict) require `&mut self`, which the
545+
// borrow checker or an external `RwLock` ensures is exclusive.
537546
unsafe impl<K, V> Sync for S3FifoCache<K, V>
538547
where
539548
K: Clone + Eq + Hash + Sync,
@@ -586,8 +595,8 @@ where
586595
///
587596
/// # Panics
588597
///
589-
/// Panics if `capacity` is zero. Debug-asserts that ratios are finite and
590-
/// non-negative.
598+
/// Panics if `capacity` is zero, if `small_ratio` is not in `[0.0, 1.0]`,
599+
/// or if `ghost_ratio` is negative or non-finite.
591600
///
592601
/// # Example
593602
///
@@ -600,12 +609,12 @@ where
600609
/// ```
601610
pub fn with_ratios(capacity: usize, small_ratio: f64, ghost_ratio: f64) -> Self {
602611
assert!(capacity > 0, "cache capacity must be greater than zero");
603-
debug_assert!(
612+
assert!(
604613
small_ratio.is_finite() && (0.0..=1.0).contains(&small_ratio),
605614
"small_ratio must be in [0.0, 1.0], got {}",
606615
small_ratio
607616
);
608-
debug_assert!(
617+
assert!(
609618
ghost_ratio.is_finite() && ghost_ratio >= 0.0,
610619
"ghost_ratio must be finite and non-negative, got {}",
611620
ghost_ratio
@@ -1449,31 +1458,34 @@ where
14491458
},
14501459
}
14511460
} else {
1452-
// Evict: freq == 0, remove from cache
1461+
// Evict: freq == 0, remove from cache.
1462+
// Pop first to take ownership of the node, then use its key to
1463+
// remove from the map and (for Small) record in the ghost list.
1464+
// This avoids cloning the key on every eviction.
14531465
match queue {
14541466
QueueKind::Small => {
14551467
#[cfg(feature = "metrics")]
14561468
{
14571469
self.metrics.small_evictions += 1;
14581470
}
14591471

1460-
// SAFETY: tail_ptr is valid, clone key before pop destroys the node
1461-
let key = unsafe { tail_ptr.as_ref().key.clone() };
1462-
self.map.remove(&key);
1463-
let _ = self.pop_small_tail();
1464-
self.ghost.record(key); // Record evicted Small items in Ghost
1472+
// Invariant: tail was Some(tail_ptr), and nothing mutated
1473+
// the queue since we checked, so pop always returns Some.
1474+
let node = self.pop_small_tail().unwrap();
1475+
self.map.remove(&node.key);
1476+
// Move key out of the box for ghost recording; drops value.
1477+
let Node { key, .. } = *node;
1478+
self.ghost.record(key);
14651479
},
14661480
QueueKind::Main => {
14671481
#[cfg(feature = "metrics")]
14681482
{
14691483
self.metrics.main_evictions += 1;
14701484
}
14711485

1472-
// SAFETY: tail_ptr is valid, clone key before pop destroys the node
1473-
let key = unsafe { tail_ptr.as_ref().key.clone() };
1474-
self.map.remove(&key);
1475-
let _ = self.pop_main_tail();
1476-
// Don't record Main evictions in Ghost
1486+
let node = self.pop_main_tail().unwrap();
1487+
self.map.remove(&node.key);
1488+
// Don't record Main evictions in Ghost; node drops here.
14771489
},
14781490
}
14791491
}

0 commit comments

Comments
 (0)