Skip to main content

pingora_cache/
predictor.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//! Cacheability Predictor
16
17use crate::hashtable::ConcurrentLruCache;
18
19pub type CustomReasonPredicate = fn(&'static str) -> bool;
20
21/// Cacheability Predictor
22///
23/// Remembers previously uncacheable assets.
24/// Allows bypassing cache / cache lock early based on historical precedent.
25///
26/// NOTE: to simply avoid caching requests with certain characteristics,
27/// add checks in request_cache_filter to avoid enabling cache in the first place.
28/// The predictor's bypass mechanism handles cases where the request _looks_ cacheable
29/// but its previous responses suggest otherwise. The request _could_ be cacheable in the future.
30pub struct Predictor<const N_SHARDS: usize> {
31    /// Maps a remembered-uncacheable key to the [NoCacheReason] it was last marked with,
32    /// so callers can tell a size-driven bypass apart from any other kind.
33    uncacheable_keys: ConcurrentLruCache<NoCacheReason, N_SHARDS>,
34    skip_custom_reasons_fn: Option<CustomReasonPredicate>,
35}
36
37use crate::{key::CacheHashKey, CacheKey, NoCacheReason};
38use log::debug;
39
40/// The cache predictor trait.
41///
42/// This trait allows user defined predictor to replace [Predictor].
43pub trait CacheablePredictor {
44    /// Return true if likely cacheable, false if likely not.
45    fn cacheable_prediction(&self, key: &CacheKey) -> bool;
46
47    /// Return the [NoCacheReason] this key is currently remembered as uncacheable for.
48    ///
49    /// Callers use this to report *why* a request bypassed the cache instead of inferring it.
50    /// `None` means the key is not remembered as uncacheable, or the implementation does not
51    /// track reasons, so callers must not assume anything about the previous response.
52    ///
53    /// The default implementation returns `None`.
54    fn predicted_uncacheable_reason(&self, _key: &CacheKey) -> Option<NoCacheReason> {
55        None
56    }
57
58    /// Mark cacheable to allow next request to cache.
59    /// Returns false if the key was already marked cacheable.
60    fn mark_cacheable(&self, key: &CacheKey) -> bool;
61
62    /// Mark uncacheable to actively bypass cache on the next request.
63    /// May skip marking on certain NoCacheReasons.
64    /// Returns None if we skipped marking uncacheable.
65    /// Returns Some(false) if the key was already marked uncacheable.
66    fn mark_uncacheable(&self, key: &CacheKey, reason: NoCacheReason) -> Option<bool>;
67}
68
69impl<const N_SHARDS: usize> Predictor<N_SHARDS> {
70    /// Create a new Predictor with `N_SHARDS * shard_capacity` total capacity for
71    /// uncacheable cache keys.
72    ///
73    /// - `shard_capacity`: defines number of keys remembered as uncacheable per LRU shard.
74    /// - `skip_custom_reasons_fn`: an optional predicate used in `mark_uncacheable`
75    ///   that can customize which `Custom` `NoCacheReason`s ought to be remembered as uncacheable.
76    ///   If the predicate returns true, then the predictor will skip remembering the current
77    ///   cache key as uncacheable (and avoid bypassing cache on the next request).
78    pub fn new(
79        shard_capacity: usize,
80        skip_custom_reasons_fn: Option<CustomReasonPredicate>,
81    ) -> Predictor<N_SHARDS> {
82        Predictor {
83            uncacheable_keys: ConcurrentLruCache::<NoCacheReason, N_SHARDS>::new(shard_capacity),
84            skip_custom_reasons_fn,
85        }
86    }
87}
88
89impl<const N_SHARDS: usize> CacheablePredictor for Predictor<N_SHARDS> {
90    fn cacheable_prediction(&self, key: &CacheKey) -> bool {
91        self.predicted_uncacheable_reason(key).is_none()
92    }
93
94    fn predicted_uncacheable_reason(&self, key: &CacheKey) -> Option<NoCacheReason> {
95        // variance key is ignored because this check happens before cache lookup
96        let hash = key.primary_bin();
97        let key = u128::from_be_bytes(hash); // Endianness doesn't matter
98
99        // Note: LRU updated in mark_* functions only,
100        // as we assume the caller always updates the cacheability of the response later.
101        // peek() reads without promoting, matching that.
102        self.uncacheable_keys.read(key).peek(&key).copied()
103    }
104
105    fn mark_cacheable(&self, key: &CacheKey) -> bool {
106        // variance key is ignored because cacheable_prediction() is called before cache lookup
107        // where the variance key is unknown
108        let hash = key.primary_bin();
109        let key = u128::from_be_bytes(hash);
110
111        let cache = self.uncacheable_keys.get(key);
112        if !cache.read().contains(&key) {
113            // not in uncacheable list, nothing to do
114            return true;
115        }
116
117        let mut cache = cache.write();
118        cache.pop(&key);
119        debug!("bypassed request became cacheable");
120        false
121    }
122
123    fn mark_uncacheable(&self, key: &CacheKey, reason: NoCacheReason) -> Option<bool> {
124        // only mark as uncacheable for the future on certain reasons,
125        // (e.g. InternalErrors)
126        use NoCacheReason::*;
127        match reason {
128            // CacheLockGiveUp: the writer will set OriginNotCache (if applicable)
129            // readers don't need to do it
130            NeverEnabled
131            | StorageError
132            | InternalError
133            | Deferred
134            | CacheLockGiveUp
135            | CacheLockTimeout
136            | CacheLockRetryLimit
137            | DeclinedToUpstream
138            | UpstreamError
139            | PredictedResponseTooLarge => {
140                return None;
141            }
142            // Skip certain NoCacheReason::Custom according to user
143            Custom(reason) if self.skip_custom_reasons_fn.is_some_and(|f| f(reason)) => {
144                return None;
145            }
146            Custom(_) | OriginNotCache | ResponseTooLarge => { /* mark uncacheable for these only */
147            }
148        }
149
150        // variance key is ignored because cacheable_prediction() is called before cache lookup
151        // where the variance key is unknown
152        let hash = key.primary_bin();
153        let key = u128::from_be_bytes(hash);
154
155        let mut cache = self.uncacheable_keys.get(key).write();
156        // put() returns Some(old_reason) if the key existed, else None.
157        // Re-marking overwrites, so the most recent reason is the one reported.
158        let new_key = cache.put(key, reason).is_none();
159        if new_key {
160            debug!("request marked uncacheable");
161        }
162        Some(new_key)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    #[test]
170    fn test_mark_cacheability() {
171        let predictor = Predictor::<1>::new(10, None);
172        let key = CacheKey::new("b", "c");
173        // cacheable if no history
174        assert!(predictor.cacheable_prediction(&key));
175
176        // don't remember internal / storage errors
177        predictor.mark_uncacheable(&key, NoCacheReason::InternalError);
178        assert!(predictor.cacheable_prediction(&key));
179        predictor.mark_uncacheable(&key, NoCacheReason::StorageError);
180        assert!(predictor.cacheable_prediction(&key));
181
182        // origin explicitly said uncacheable
183        predictor.mark_uncacheable(&key, NoCacheReason::OriginNotCache);
184        assert!(!predictor.cacheable_prediction(&key));
185
186        // mark cacheable again
187        predictor.mark_cacheable(&key);
188        assert!(predictor.cacheable_prediction(&key));
189    }
190
191    #[test]
192    fn test_remembers_uncacheable_reason() {
193        let predictor = Predictor::<1>::new(10, None);
194        let key = CacheKey::new("reason", "tag");
195        assert_eq!(predictor.predicted_uncacheable_reason(&key), None);
196
197        predictor.mark_uncacheable(&key, NoCacheReason::Custom("AuthorizationHeader"));
198        assert_eq!(
199            predictor.predicted_uncacheable_reason(&key),
200            Some(NoCacheReason::Custom("AuthorizationHeader"))
201        );
202
203        // re-marking replaces the reason
204        predictor.mark_uncacheable(&key, NoCacheReason::ResponseTooLarge);
205        assert_eq!(
206            predictor.predicted_uncacheable_reason(&key),
207            Some(NoCacheReason::ResponseTooLarge)
208        );
209
210        // a skipped reason leaves the remembered one alone
211        predictor.mark_uncacheable(&key, NoCacheReason::InternalError);
212        assert_eq!(
213            predictor.predicted_uncacheable_reason(&key),
214            Some(NoCacheReason::ResponseTooLarge)
215        );
216
217        predictor.mark_cacheable(&key);
218        assert_eq!(predictor.predicted_uncacheable_reason(&key), None);
219    }
220
221    #[test]
222    fn test_custom_skip_predicate() {
223        let predictor = Predictor::<1>::new(
224            10,
225            Some(|custom_reason| matches!(custom_reason, "Skipping")),
226        );
227        let key = CacheKey::new("b", "c");
228        // cacheable if no history
229        assert!(predictor.cacheable_prediction(&key));
230
231        // custom predicate still uses default skip reasons
232        predictor.mark_uncacheable(&key, NoCacheReason::InternalError);
233        assert!(predictor.cacheable_prediction(&key));
234
235        // other custom reasons can still be marked uncacheable
236        predictor.mark_uncacheable(&key, NoCacheReason::Custom("DontCacheMe"));
237        assert!(!predictor.cacheable_prediction(&key));
238
239        let key = CacheKey::new("c", "d");
240        assert!(predictor.cacheable_prediction(&key));
241        // specific custom reason is skipped
242        predictor.mark_uncacheable(&key, NoCacheReason::Custom("Skipping"));
243        assert!(predictor.cacheable_prediction(&key));
244    }
245
246    #[test]
247    fn test_mark_uncacheable_lru() {
248        let predictor = Predictor::<1>::new(3, None);
249        let key1 = CacheKey::new("b", "c");
250        predictor.mark_uncacheable(&key1, NoCacheReason::OriginNotCache);
251        assert!(!predictor.cacheable_prediction(&key1));
252
253        let key2 = CacheKey::new("bc", "c");
254        predictor.mark_uncacheable(&key2, NoCacheReason::OriginNotCache);
255        assert!(!predictor.cacheable_prediction(&key2));
256
257        let key3 = CacheKey::new("cd", "c");
258        predictor.mark_uncacheable(&key3, NoCacheReason::OriginNotCache);
259        assert!(!predictor.cacheable_prediction(&key3));
260
261        // promote / reinsert key1
262        predictor.mark_uncacheable(&key1, NoCacheReason::OriginNotCache);
263
264        let key4 = CacheKey::new("de", "c");
265        predictor.mark_uncacheable(&key4, NoCacheReason::OriginNotCache);
266        assert!(!predictor.cacheable_prediction(&key4));
267
268        // key 1 was recently used
269        assert!(!predictor.cacheable_prediction(&key1));
270        // key 2 was evicted
271        assert!(predictor.cacheable_prediction(&key2));
272        assert!(!predictor.cacheable_prediction(&key3));
273        assert!(!predictor.cacheable_prediction(&key4));
274    }
275
276    #[test]
277    fn test_shard_count_above_32() {
278        // The stdlib only auto-derives `Default` for arrays up to N=32, which
279        // previously capped the shard count. This exercises a shard count well
280        // above 32 to ensure the `arrayvec`-based construction supports any N.
281        let predictor = Predictor::<64>::new(10, None);
282        let key = CacheKey::new("b", "c");
283        assert!(predictor.cacheable_prediction(&key));
284
285        predictor.mark_uncacheable(&key, NoCacheReason::OriginNotCache);
286        assert!(!predictor.cacheable_prediction(&key));
287
288        predictor.mark_cacheable(&key);
289        assert!(predictor.cacheable_prediction(&key));
290    }
291}