Skip to main content

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}