Skip to main content

pingora_cache/
admission.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//! Admission policies for deciding whether cache misses should be stored.
16
17#[cfg(test)]
18use crate::key::CompactCacheKey;
19use crate::CacheKey;
20#[cfg(test)]
21use std::collections::HashMap;
22#[cfg(test)]
23use std::num::NonZeroU32;
24#[cfg(test)]
25use std::sync::Mutex;
26
27/// The result of observing an absent cache key.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Decision {
30    /// Skip cache admission for this request.
31    Defer {
32        /// Number of observations reported by the policy.
33        observed: u32,
34    },
35    /// Allow the request to proceed through the normal miss and fill path.
36    Ready {
37        /// Number of observations reported by the policy.
38        observed: u32,
39    },
40}
41
42impl Decision {
43    /// Return the number of observations reported by the policy.
44    pub fn observed(self) -> u32 {
45        match self {
46            Self::Defer { observed } | Self::Ready { observed } => observed,
47        }
48    }
49
50    /// Whether admission should be deferred.
51    pub fn is_deferred(self) -> bool {
52        matches!(self, Self::Defer { .. })
53    }
54}
55
56/// Policy invoked after storage reports that a cache key is absent.
57pub trait AdmissionPolicy: Send + Sync {
58    /// Observe an absent key and return a [`Decision`] for this request.
59    ///
60    /// This method runs synchronously in the asynchronous cache lookup hot path. Implementations
61    /// must be fast and non-blocking, and must not panic.
62    ///
63    /// Policies that retain the key beyond this call may convert it to a compact or
64    /// policy-specific representation.
65    fn observe(&self, key: &CacheKey) -> Decision;
66}
67
68/// Simple configurable policy used to exercise admission plumbing in tests.
69#[cfg(test)]
70pub(crate) struct MinUsesAdmissionPolicy {
71    min_uses: NonZeroU32,
72    observations: Mutex<HashMap<CompactCacheKey, u32>>,
73}
74
75#[cfg(test)]
76impl MinUsesAdmissionPolicy {
77    pub(crate) fn new(min_uses: NonZeroU32) -> Self {
78        Self {
79            min_uses,
80            observations: Mutex::new(HashMap::new()),
81        }
82    }
83}
84
85#[cfg(test)]
86impl AdmissionPolicy for MinUsesAdmissionPolicy {
87    fn observe(&self, key: &CacheKey) -> Decision {
88        let mut observations = self.observations.lock().unwrap();
89        let observed = observations
90            .entry(key.to_compact())
91            .and_modify(|observed| *observed = observed.saturating_add(1).min(self.min_uses.get()))
92            .or_insert(1);
93        if *observed >= self.min_uses.get() {
94            Decision::Ready {
95                observed: *observed,
96            }
97        } else {
98            Decision::Defer {
99                observed: *observed,
100            }
101        }
102    }
103}