pingora_cache/eviction/mod.rs
1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Cache eviction module
16
17use crate::key::CompactCacheKey;
18
19use async_trait::async_trait;
20use pingora_error::Result;
21use serde::ser::SerializeTuple;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23use std::fmt::{Display, Formatter, Result as FmtResult};
24use std::hash::{Hash, Hasher};
25use std::time::SystemTime;
26
27pub mod async_lru;
28pub mod lru;
29pub mod simple_lru;
30
31/// Storage-defined identity for a cache entry.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
33pub struct CacheEntryId(u64);
34
35impl CacheEntryId {
36 /// Construct an entry ID from its storage-defined value.
37 pub fn new(value: u64) -> Self {
38 Self(value)
39 }
40
41 /// Return the storage-defined value.
42 pub fn get(self) -> u64 {
43 self.0
44 }
45}
46
47/// The identity of an entry tracked by an eviction manager.
48///
49/// Identified entries combine a logical cache key with storage-defined entry identity. Eviction
50/// managers paired with storage that returns identified entries must preserve and compare the
51/// complete key. Managers that use only the compact key cannot correctly account for such storage.
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
53pub enum CacheEntryKey {
54 /// An entry identified only by its logical cache key.
55 KeyOnly(CompactCacheKey),
56 /// An entry with additional storage-defined identity.
57 Identified {
58 /// The logical cache key.
59 key: CompactCacheKey,
60 /// The storage-defined entry ID.
61 id: CacheEntryId,
62 },
63}
64
65impl Hash for CacheEntryKey {
66 fn hash<H: Hasher>(&self, state: &mut H) {
67 CacheEntryKeyRef::from(self).hash(state);
68 }
69}
70
71impl Serialize for CacheEntryKey {
72 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
73 where
74 S: Serializer,
75 {
76 match self {
77 Self::KeyOnly(key) => key.serialize(serializer),
78 Self::Identified { key, id } => {
79 let mut tuple = serializer.serialize_tuple(2)?;
80 tuple.serialize_element(key)?;
81 tuple.serialize_element(id)?;
82 tuple.end()
83 }
84 }
85 }
86}
87
88impl<'de> Deserialize<'de> for CacheEntryKey {
89 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90 where
91 D: Deserializer<'de>,
92 {
93 CacheEntryKeyRepr::deserialize(deserializer).map(|entry| match entry {
94 CacheEntryKeyRepr::Identified(key, id) => Self::from_entry_id(key, id),
95 CacheEntryKeyRepr::KeyOnly(key) => Self::KeyOnly(key),
96 })
97 }
98}
99
100#[derive(Deserialize)]
101#[serde(untagged)]
102enum CacheEntryKeyRepr {
103 Identified(CompactCacheKey, Option<CacheEntryId>),
104 KeyOnly(CompactCacheKey),
105}
106
107impl Display for CacheEntryKey {
108 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
109 write!(f, "{}", self.key())?;
110 if let Some(id) = self.entry_id() {
111 write!(f, ", entry ID: {:x}", id.get())?;
112 }
113 Ok(())
114 }
115}
116
117impl Default for CacheEntryKey {
118 fn default() -> Self {
119 Self::KeyOnly(CompactCacheKey::default())
120 }
121}
122
123impl CacheEntryKey {
124 /// Construct an entry from its logical key and optional storage-defined identity.
125 pub fn from_entry_id(key: CompactCacheKey, id: Option<CacheEntryId>) -> Self {
126 match id {
127 Some(id) => Self::Identified { key, id },
128 None => Self::KeyOnly(key),
129 }
130 }
131
132 /// Construct an entry identified only by its logical cache key.
133 pub fn key_only(key: CompactCacheKey) -> Self {
134 Self::KeyOnly(key)
135 }
136
137 /// Construct an entry with storage-defined identity.
138 pub fn identified(key: CompactCacheKey, id: CacheEntryId) -> Self {
139 Self::Identified { key, id }
140 }
141
142 /// Return the underlying cache key.
143 pub fn key(&self) -> &CompactCacheKey {
144 match self {
145 Self::KeyOnly(key) | Self::Identified { key, .. } => key,
146 }
147 }
148
149 /// Consume the eviction key and return its underlying cache key.
150 pub fn into_key(self) -> CompactCacheKey {
151 match self {
152 Self::KeyOnly(key) | Self::Identified { key, .. } => key,
153 }
154 }
155
156 /// Return the storage-defined entry ID, if present.
157 pub fn entry_id(&self) -> Option<CacheEntryId> {
158 match self {
159 Self::KeyOnly(_) => None,
160 Self::Identified { id, .. } => Some(*id),
161 }
162 }
163}
164
165/// A borrowed cache entry identity used for exact eviction-manager lookups.
166///
167/// This type hashes identically to the equivalent owned [`CacheEntryKey`].
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum CacheEntryKeyRef<'a> {
170 /// An entry identified only by its logical cache key.
171 KeyOnly(&'a CompactCacheKey),
172 /// An entry with additional storage-defined identity.
173 Identified {
174 /// The logical cache key.
175 key: &'a CompactCacheKey,
176 /// The storage-defined entry ID.
177 id: CacheEntryId,
178 },
179}
180
181impl<'a> CacheEntryKeyRef<'a> {
182 /// Construct a borrowed entry identity from its logical key and storage-defined ID, if any.
183 ///
184 /// The ID must match the entry originally admitted to an eviction manager. A different ID is a
185 /// different entry and will not remove or otherwise match the admitted entry.
186 pub fn from_entry_id(key: &'a CompactCacheKey, id: Option<CacheEntryId>) -> Self {
187 match id {
188 Some(id) => Self::Identified { key, id },
189 None => Self::KeyOnly(key),
190 }
191 }
192
193 /// Return the underlying cache key.
194 pub fn key(self) -> &'a CompactCacheKey {
195 match self {
196 Self::KeyOnly(key) | Self::Identified { key, .. } => key,
197 }
198 }
199
200 /// Return the storage-defined entry ID, if present.
201 pub fn entry_id(self) -> Option<CacheEntryId> {
202 match self {
203 Self::KeyOnly(_) => None,
204 Self::Identified { id, .. } => Some(id),
205 }
206 }
207}
208
209impl Display for CacheEntryKeyRef<'_> {
210 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
211 write!(f, "{}", self.key())?;
212 if let Some(id) = self.entry_id() {
213 write!(f, ", entry ID: {:x}", id.get())?;
214 }
215 Ok(())
216 }
217}
218
219impl<'a> From<&'a CacheEntryKey> for CacheEntryKeyRef<'a> {
220 fn from(entry: &'a CacheEntryKey) -> Self {
221 Self::from_entry_id(entry.key(), entry.entry_id())
222 }
223}
224
225/// The trait that a cache eviction algorithm needs to implement.
226///
227/// NOTE: these trait methods require &self not &mut self, which means concurrency should
228/// be handled the implementations internally.
229#[async_trait]
230pub trait EvictionManager: Send + Sync {
231 /// Total size of the cache in bytes tracked by this eviction manager
232 fn total_size(&self) -> usize;
233 /// Number of assets tracked by this eviction manager
234 fn total_items(&self) -> usize;
235 /// Number of bytes that are already evicted
236 ///
237 /// The accumulated number is returned to play well with Prometheus counter metric type.
238 fn evicted_size(&self) -> usize;
239 /// Number of assets that are already evicted
240 ///
241 /// The accumulated number is returned to play well with Prometheus counter metric type.
242 fn evicted_items(&self) -> usize;
243
244 /// Admit an item
245 ///
246 /// Return one or more items to evict. The sizes of these items are deducted
247 /// from the total size already. The caller needs to make sure that these assets are actually
248 /// removed from the storage.
249 ///
250 /// If the item is already admitted, A. update its freshness; B. if the new size is larger than the
251 /// existing one, Some(_) might be returned for the caller to evict.
252 fn admit(
253 &self,
254 item: CacheEntryKey,
255 size: usize,
256 fresh_until: SystemTime,
257 ) -> Vec<CacheEntryKey>;
258
259 /// Adjust an item's weight upwards by a delta. If the item is not already admitted,
260 /// track it with the delta as its initial weight, capped by `max_weight`, and floored to 1.
261 ///
262 /// An optional `max_weight` hint indicates the known max weight of the current key in case the
263 /// weight should not be incremented above this amount. This hint will not shrink an item whose
264 /// current weight already exceeds `max_weight`.
265 ///
266 /// Return one or more items to evict. The sizes of these items are deducted
267 /// from the total size already. The caller needs to make sure that these assets are actually
268 /// removed from the storage.
269 fn increment_weight(
270 &self,
271 item: &CacheEntryKey,
272 delta: usize,
273 max_weight: Option<usize>,
274 ) -> Vec<CacheEntryKey>;
275
276 /// Remove an item from the eviction manager.
277 ///
278 /// The size of the item will be deducted. Implementations must require the complete identity to
279 /// match the originally admitted entry; a key-only identity must not match an identified entry
280 /// with the same logical key.
281 fn remove(&self, item: CacheEntryKeyRef<'_>);
282
283 /// Access an item that should already be in cache.
284 ///
285 /// If the item is not tracked by this [EvictionManager], track it but no eviction will happen.
286 ///
287 /// The call used for asking the eviction manager to track the assets that are already admitted
288 /// in the cache storage system.
289 fn access(&self, item: &CacheEntryKey, size: usize, fresh_until: SystemTime) -> bool;
290
291 /// Peek into the manager to see if the item is already tracked by the system
292 ///
293 /// This function should have no side-effect on the asset itself. For example, for LRU, this
294 /// method shouldn't change the popularity of the asset being peeked.
295 fn peek(&self, item: &CacheEntryKey) -> bool;
296
297 /// Serialize to save the state of this eviction manager to disk
298 ///
299 /// This function is for preserving the eviction manager's state across server restarts.
300 ///
301 /// `dir_path` define the directory on disk that the data should use.
302 // dir_path is &str no AsRef<Path> so that trait objects can be used
303 async fn save(&self, dir_path: &str) -> Result<()>;
304
305 /// The counterpart of [Self::save()].
306 async fn load(&self, dir_path: &str) -> Result<()>;
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::CacheKey;
313
314 #[test]
315 fn cache_entry_key_serde_preserves_identity() {
316 let key = CacheKey::new("entry", "1").to_compact();
317 let legacy = rmp_serde::to_vec(&key).unwrap();
318 let key_only = CacheEntryKey::key_only(key.clone());
319 assert_eq!(rmp_serde::to_vec(&key_only).unwrap(), legacy);
320 assert_eq!(
321 rmp_serde::from_slice::<CacheEntryKey>(&legacy).unwrap(),
322 key_only
323 );
324 let previous_key_only =
325 rmp_serde::to_vec(&(key.clone(), Option::<CacheEntryId>::None)).unwrap();
326 assert_eq!(
327 rmp_serde::from_slice::<CacheEntryKey>(&previous_key_only).unwrap(),
328 key_only
329 );
330
331 let entry = CacheEntryKey::identified(key, CacheEntryId::new(7));
332
333 let serialized = rmp_serde::to_vec(&entry).unwrap();
334 let deserialized = rmp_serde::from_slice(&serialized).unwrap();
335
336 assert_eq!(entry, deserialized);
337 }
338
339 #[test]
340 fn owned_and_borrowed_entry_keys_hash_identically() {
341 use ahash::AHasher;
342 use std::collections::hash_map::DefaultHasher;
343 use std::hash::{Hash, Hasher};
344
345 // This compares structural identity through each hasher, not hash stability across versions.
346 let key = CacheKey::new("entry", "1").to_compact();
347 for entry in [
348 CacheEntryKey::key_only(key.clone()),
349 CacheEntryKey::identified(key, CacheEntryId::new(7)),
350 ] {
351 let mut owned = DefaultHasher::new();
352 entry.hash(&mut owned);
353 let mut borrowed = DefaultHasher::new();
354 CacheEntryKeyRef::from(&entry).hash(&mut borrowed);
355 assert_eq!(owned.finish(), borrowed.finish());
356
357 let mut owned = AHasher::default();
358 entry.hash(&mut owned);
359 let mut borrowed = AHasher::default();
360 CacheEntryKeyRef::from(&entry).hash(&mut borrowed);
361 assert_eq!(owned.finish(), borrowed.finish());
362 }
363 }
364}