From 783663182cf6a743e9a741ac564a3c2b75ab4339 Mon Sep 17 00:00:00 2001 From: Deniz Date: Thu, 7 May 2026 18:34:37 -0700 Subject: [PATCH 1/5] extend lifecycle trait with on_evict_(cold|hot) --- src/lib.rs | 18 ++++++++++++++++++ src/shard.rs | 21 +++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7840608..aa3b764 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,8 +196,26 @@ pub trait Lifecycle { fn before_evict(&self, state: &mut Self::RequestState, key: &Key, val: &mut Val) {} /// Called when an item is evicted. + #[deprecated( + since = "0.6.22", + note = "Use `on_evict_hot` or `on_evict_cold` instead, depending on the desired semantics. This method will still be called by default to preserve backwards compatibility, but it won't be called if either of the new methods are implemented." + )] fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val); + /// Called when an item is evicted from the cold queue. + #[inline] + fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) { + #[allow(deprecated)] + self.on_evict(state, key, val) + } + + /// Called when an item is evicted from the hot queue. + #[inline] + fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { + #[allow(deprecated)] + self.on_evict(state, key, val) + } + /// Called after a request finishes, e.g.: insert, replace. /// /// Notes: diff --git a/src/shard.rs b/src/shard.rs index ec40b94..0ae18d9 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -783,7 +783,8 @@ impl< if self.num_non_resident > self.capacity_non_resident { self.advance_ghost(); } - self.lifecycle.on_evict(lcs, evicted.key, evicted.value); + self.lifecycle + .on_evict_cold(lcs, evicted.key, evicted.value); return true; } } @@ -835,7 +836,7 @@ impl< unsafe { core::hint::unreachable_unchecked() }; }; self.hot_head = next; - self.lifecycle.on_evict(lcs, evicted.key, evicted.value); + self.lifecycle.on_evict_hot(lcs, evicted.key, evicted.value); self.map_remove(hash, idx); } return true; @@ -920,7 +921,15 @@ impl< } else if evicted_weight != 0 && weight == 0 { *list_head = self.entries.unlink(idx); } - self.lifecycle.on_evict(lcs, evicted.key, evicted.value); + match enter_state { + ResidentState::Hot => { + self.lifecycle.on_evict_hot(lcs, evicted.key, evicted.value) + } + ResidentState::Cold => { + self.lifecycle + .on_evict_cold(lcs, evicted.key, evicted.value) + } + } } Entry::Ghost(_) => { self.weight_hot += weight; @@ -1052,7 +1061,7 @@ impl< ) -> Result<(), Val> { self.entries.remove(placeholder.idx()); self.map_remove(placeholder.hash(), placeholder.idx()); - self.lifecycle.on_evict(lcs, key, value); + self.lifecycle.on_evict_hot(lcs, key, value); Ok(()) } @@ -1122,13 +1131,13 @@ impl< // Make sure to remove any existing entry if let Some((idx, _)) = self.search_resident(hash, &key) { if let Some((ek, ev)) = self.remove_internal(hash, idx) { - self.lifecycle.on_evict(lcs, ek, ev); + self.lifecycle.on_evict_hot(lcs, ek, ev); } } if matches!(strategy, InsertStrategy::Replace { .. }) { return Err((key, value)); } - self.lifecycle.on_evict(lcs, key, value); + self.lifecycle.on_evict_hot(lcs, key, value); Ok(()) } From 21a4e632fc95decb9ce20a089b9e9bc0d6a6325f Mon Sep 17 00:00:00 2001 From: Deniz Date: Thu, 7 May 2026 19:02:55 -0700 Subject: [PATCH 2/5] default impl for on_evict --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index aa3b764..35c7441 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,7 +200,8 @@ pub trait Lifecycle { since = "0.6.22", note = "Use `on_evict_hot` or `on_evict_cold` instead, depending on the desired semantics. This method will still be called by default to preserve backwards compatibility, but it won't be called if either of the new methods are implemented." )] - fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val); + #[allow(unused_variables)] + fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) {} /// Called when an item is evicted from the cold queue. #[inline] From 141ff4072aecb4c1ab9db996c0b64dd0ddfb1108 Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Sat, 16 May 2026 00:09:54 +0200 Subject: [PATCH 3/5] Address Copilot review on PR #117 - Fix hot/cold misclassification in handle_insert_overweight: capture the evicted entry's ResidentState before remove_internal so cold evictions aren't reported as hot. - Reword on_evict deprecation note to accurately describe per-method fallback (each new method delegates to on_evict independently). - Document rejection-style behavior on on_evict_hot: items that never enter the cache (oversized inserts and oversized placeholder values) are reported as hot. - Migrate DefaultLifecycle (sync and unsync) and the eviction_listener example to implement on_evict_hot/on_evict_cold directly. --- examples/eviction_listener.rs | 6 +++++- src/lib.rs | 14 +++++++++++++- src/shard.rs | 8 ++++++-- src/sync.rs | 28 ++++++++++++++++++++-------- src/unsync.rs | 3 --- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/examples/eviction_listener.rs b/examples/eviction_listener.rs index 4861993..f32e3e5 100644 --- a/examples/eviction_listener.rs +++ b/examples/eviction_listener.rs @@ -9,7 +9,11 @@ impl Lifecycle for EvictionListener { fn begin_request(&self) -> Self::RequestState {} - fn on_evict(&self, _state: &mut Self::RequestState, key: u64, val: u64) { + fn on_evict_hot(&self, _state: &mut Self::RequestState, key: u64, val: u64) { + let _ = self.0.send((key, val)); + } + + fn on_evict_cold(&self, _state: &mut Self::RequestState, key: u64, val: u64) { let _ = self.0.send((key, val)); } } diff --git a/src/lib.rs b/src/lib.rs index 35c7441..6bbb716 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,14 +196,20 @@ pub trait Lifecycle { fn before_evict(&self, state: &mut Self::RequestState, key: &Key, val: &mut Val) {} /// Called when an item is evicted. + /// + /// To receive eviction notifications, implement at least one of `on_evict_hot` + /// or `on_evict_cold`, or this deprecated `on_evict` (for backwards compatibility). + /// If none are implemented, evictions are silently dropped. #[deprecated( since = "0.6.22", - note = "Use `on_evict_hot` or `on_evict_cold` instead, depending on the desired semantics. This method will still be called by default to preserve backwards compatibility, but it won't be called if either of the new methods are implemented." + note = "Use `on_evict_hot` and `on_evict_cold` instead. By default, each of the new methods falls back to `on_evict` individually; overriding only one (e.g. `on_evict_hot`) leaves the other still delegating to `on_evict`." )] #[allow(unused_variables)] fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) {} /// Called when an item is evicted from the cold queue. + /// + /// By default delegates to the deprecated `on_evict`. #[inline] fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) { #[allow(deprecated)] @@ -211,6 +217,12 @@ pub trait Lifecycle { } /// Called when an item is evicted from the hot queue. + /// + /// By default delegates to the deprecated `on_evict`. + /// + /// Note: items that are rejected without ever being admitted to the cache + /// (oversized inserts and oversized placeholder values) are reported via this + /// method, since new admissions target the hot queue first. #[inline] fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { #[allow(deprecated)] diff --git a/src/shard.rs b/src/shard.rs index 0ae18d9..566833a 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -1129,9 +1129,13 @@ impl< strategy: InsertStrategy, ) -> Result<(), (Key, Val)> { // Make sure to remove any existing entry - if let Some((idx, _)) = self.search_resident(hash, &key) { + if let Some((idx, resident)) = self.search_resident(hash, &key) { + let prev_state = resident.state; if let Some((ek, ev)) = self.remove_internal(hash, idx) { - self.lifecycle.on_evict_hot(lcs, ek, ev); + match prev_state { + ResidentState::Hot => self.lifecycle.on_evict_hot(lcs, ek, ev), + ResidentState::Cold => self.lifecycle.on_evict_cold(lcs, ek, ev), + } } } if matches!(strategy, InsertStrategy::Replace { .. }) { diff --git a/src/sync.rs b/src/sync.rs index 52cd3a4..5dcd33d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -809,6 +809,19 @@ impl Clone for DefaultLifecycle { } } +impl DefaultLifecycle { + #[inline] + fn record_evict(&self, state: &mut [Option<(Key, Val)>; 2], key: Key, val: Val) { + if std::mem::needs_drop::<(Key, Val)>() { + if state[0].is_none() { + state[0] = Some((key, val)); + } else if state[1].is_none() { + state[1] = Some((key, val)); + } + } + } +} + impl Lifecycle for DefaultLifecycle { // Why two items? // Because assuming the cache has roughly similarly weighted items, @@ -823,14 +836,13 @@ impl Lifecycle for DefaultLifecycle { } #[inline] - fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) { - if std::mem::needs_drop::<(Key, Val)>() { - if state[0].is_none() { - state[0] = Some((key, val)); - } else if state[1].is_none() { - state[1] = Some((key, val)); - } - } + fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { + self.record_evict(state, key, val); + } + + #[inline] + fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) { + self.record_evict(state, key, val); } } diff --git a/src/unsync.rs b/src/unsync.rs index 66ee954..ae7f1d5 100644 --- a/src/unsync.rs +++ b/src/unsync.rs @@ -443,9 +443,6 @@ impl Lifecycle for DefaultLifecycle { #[inline] fn begin_request(&self) -> Self::RequestState {} - - #[inline] - fn on_evict(&self, _state: &mut Self::RequestState, _key: Key, _val: Val) {} } #[derive(Debug, Clone)] From d5a8d682c6c958782e618f897b49f9e3eecf6370 Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Sat, 16 May 2026 00:17:30 +0200 Subject: [PATCH 4/5] Lifecycle: drop on_evict deprecation; route overweight rejections to cold - on_evict stays as a provided method with an empty default body (no deprecation). on_evict_hot/on_evict_cold default to delegating to it, so existing impls keep working and new impls can override just the hot/cold methods when they want to distinguish. - Route oversized inserts and oversized placeholder rejections through on_evict_cold (they never entered any queue). - Revert DefaultLifecycle (sync/unsync) and the eviction_listener example to the simple on_evict form. --- examples/eviction_listener.rs | 6 +----- src/lib.rs | 28 +++++++++++++--------------- src/shard.rs | 4 ++-- src/sync.rs | 28 ++++++++-------------------- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/examples/eviction_listener.rs b/examples/eviction_listener.rs index f32e3e5..4861993 100644 --- a/examples/eviction_listener.rs +++ b/examples/eviction_listener.rs @@ -9,11 +9,7 @@ impl Lifecycle for EvictionListener { fn begin_request(&self) -> Self::RequestState {} - fn on_evict_hot(&self, _state: &mut Self::RequestState, key: u64, val: u64) { - let _ = self.0.send((key, val)); - } - - fn on_evict_cold(&self, _state: &mut Self::RequestState, key: u64, val: u64) { + fn on_evict(&self, _state: &mut Self::RequestState, key: u64, val: u64) { let _ = self.0.send((key, val)); } } diff --git a/src/lib.rs b/src/lib.rs index 6bbb716..36e0412 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -197,35 +197,33 @@ pub trait Lifecycle { /// Called when an item is evicted. /// - /// To receive eviction notifications, implement at least one of `on_evict_hot` - /// or `on_evict_cold`, or this deprecated `on_evict` (for backwards compatibility). - /// If none are implemented, evictions are silently dropped. - #[deprecated( - since = "0.6.22", - note = "Use `on_evict_hot` and `on_evict_cold` instead. By default, each of the new methods falls back to `on_evict` individually; overriding only one (e.g. `on_evict_hot`) leaves the other still delegating to `on_evict`." - )] + /// To distinguish evictions from the hot vs cold queues, override + /// [`Lifecycle::on_evict_hot`] and/or [`Lifecycle::on_evict_cold`] instead; + /// they default to delegating here. + /// + /// If none of `on_evict`, `on_evict_hot`, or `on_evict_cold` is overridden, + /// eviction notifications are silently dropped. #[allow(unused_variables)] + #[inline] fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) {} /// Called when an item is evicted from the cold queue. /// - /// By default delegates to the deprecated `on_evict`. + /// By default delegates to [`Lifecycle::on_evict`]. + /// + /// Note: items that are rejected without ever being admitted to the cache + /// (oversized inserts and oversized placeholder values) are also reported + /// via this method. #[inline] fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) { - #[allow(deprecated)] self.on_evict(state, key, val) } /// Called when an item is evicted from the hot queue. /// - /// By default delegates to the deprecated `on_evict`. - /// - /// Note: items that are rejected without ever being admitted to the cache - /// (oversized inserts and oversized placeholder values) are reported via this - /// method, since new admissions target the hot queue first. + /// By default delegates to [`Lifecycle::on_evict`]. #[inline] fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { - #[allow(deprecated)] self.on_evict(state, key, val) } diff --git a/src/shard.rs b/src/shard.rs index 566833a..bf01c02 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -1061,7 +1061,7 @@ impl< ) -> Result<(), Val> { self.entries.remove(placeholder.idx()); self.map_remove(placeholder.hash(), placeholder.idx()); - self.lifecycle.on_evict_hot(lcs, key, value); + self.lifecycle.on_evict_cold(lcs, key, value); Ok(()) } @@ -1141,7 +1141,7 @@ impl< if matches!(strategy, InsertStrategy::Replace { .. }) { return Err((key, value)); } - self.lifecycle.on_evict_hot(lcs, key, value); + self.lifecycle.on_evict_cold(lcs, key, value); Ok(()) } diff --git a/src/sync.rs b/src/sync.rs index 5dcd33d..52cd3a4 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -809,19 +809,6 @@ impl Clone for DefaultLifecycle { } } -impl DefaultLifecycle { - #[inline] - fn record_evict(&self, state: &mut [Option<(Key, Val)>; 2], key: Key, val: Val) { - if std::mem::needs_drop::<(Key, Val)>() { - if state[0].is_none() { - state[0] = Some((key, val)); - } else if state[1].is_none() { - state[1] = Some((key, val)); - } - } - } -} - impl Lifecycle for DefaultLifecycle { // Why two items? // Because assuming the cache has roughly similarly weighted items, @@ -836,13 +823,14 @@ impl Lifecycle for DefaultLifecycle { } #[inline] - fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { - self.record_evict(state, key, val); - } - - #[inline] - fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) { - self.record_evict(state, key, val); + fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) { + if std::mem::needs_drop::<(Key, Val)>() { + if state[0].is_none() { + state[0] = Some((key, val)); + } else if state[1].is_none() { + state[1] = Some((key, val)); + } + } } } From 3a5f57a0d03ff2156ddb1d9aa17afc7158e9c0a4 Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Sat, 16 May 2026 00:39:49 +0200 Subject: [PATCH 5/5] Cross-reference rejection routing on on_evict and on_evict_hot docs --- src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 36e0412..433d0f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,6 +203,10 @@ pub trait Lifecycle { /// /// If none of `on_evict`, `on_evict_hot`, or `on_evict_cold` is overridden, /// eviction notifications are silently dropped. + /// + /// Note: items that are rejected without ever being admitted to the cache + /// (oversized inserts and oversized placeholder values) are routed through + /// [`Lifecycle::on_evict_cold`], which by default reaches this method. #[allow(unused_variables)] #[inline] fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) {} @@ -222,6 +226,9 @@ pub trait Lifecycle { /// Called when an item is evicted from the hot queue. /// /// By default delegates to [`Lifecycle::on_evict`]. + /// + /// Note: rejected (never-admitted) items are reported via + /// [`Lifecycle::on_evict_cold`], not this method. #[inline] fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) { self.on_evict(state, key, val)