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}