@@ -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`.
529532unsafe impl < K , V > Send for S3FifoCache < K , V >
530533where
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.
537546unsafe impl < K , V > Sync for S3FifoCache < K , V >
538547where
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