pingora_cache/
key.rs

1// Copyright 2025 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 key
16
17use super::*;
18
19use blake2::{Blake2b, Digest};
20use http::Extensions;
21use serde::{Deserialize, Serialize};
22use std::fmt::{Display, Formatter, Result as FmtResult};
23
24// 16-byte / 128-bit key: large enough to avoid collision
25const KEY_SIZE: usize = 16;
26
27/// An 128 bit hash binary
28pub type HashBinary = [u8; KEY_SIZE];
29
30fn hex2str(hex: &[u8]) -> String {
31    use std::fmt::Write;
32    let mut s = String::with_capacity(KEY_SIZE * 2);
33    for c in hex {
34        write!(s, "{:02x}", c).unwrap(); // safe, just dump hex to string
35    }
36    s
37}
38
39/// Decode the hex str into [HashBinary].
40///
41/// Return `None` when the decode fails or the input is not exact 32 (to decode to 16 bytes).
42pub fn str2hex(s: &str) -> Option<HashBinary> {
43    if s.len() != KEY_SIZE * 2 {
44        return None;
45    }
46    let mut output = [0; KEY_SIZE];
47    // no need to bubble the error, it should be obvious why the decode fails
48    hex::decode_to_slice(s.as_bytes(), &mut output).ok()?;
49    Some(output)
50}
51
52/// The trait for cache key
53pub trait CacheHashKey {
54    /// Return the hash of the cache key
55    fn primary_bin(&self) -> HashBinary;
56
57    /// Return the variance hash of the cache key.
58    ///
59    /// `None` if no variance.
60    fn variance_bin(&self) -> Option<HashBinary>;
61
62    /// Return the hash including both primary and variance keys
63    fn combined_bin(&self) -> HashBinary {
64        let key = self.primary_bin();
65        if let Some(v) = self.variance_bin() {
66            let mut hasher = Blake2b128::new();
67            hasher.update(key);
68            hasher.update(v);
69            hasher.finalize().into()
70        } else {
71            // if there is no variance, combined_bin should return the same as primary_bin
72            key
73        }
74    }
75
76    /// An extra tag for identifying users
77    ///
78    /// For example, if the storage backend implements per user quota, this tag can be used.
79    fn user_tag(&self) -> &str;
80
81    /// The hex string of [Self::primary_bin()]
82    fn primary(&self) -> String {
83        hex2str(&self.primary_bin())
84    }
85
86    /// The hex string of [Self::variance_bin()]
87    fn variance(&self) -> Option<String> {
88        self.variance_bin().as_ref().map(|b| hex2str(&b[..]))
89    }
90
91    /// The hex string of [Self::combined_bin()]
92    fn combined(&self) -> String {
93        hex2str(&self.combined_bin())
94    }
95}
96
97/// General purpose cache key
98#[derive(Debug, Clone)]
99pub struct CacheKey {
100    // All strings for now. It can be more structural as long as it can hash
101    namespace: String,
102    primary: String,
103    primary_bin_override: Option<HashBinary>,
104    variance: Option<HashBinary>,
105    /// An extra tag for identifying users
106    ///
107    /// For example, if the storage backend implements per user quota, this tag can be used.
108    pub user_tag: String,
109
110    /// Grab-bag for user-defined extensions. These will not be persisted to disk.
111    pub extensions: Extensions,
112}
113
114impl CacheKey {
115    /// Set the value of the variance hash
116    pub fn set_variance_key(&mut self, key: HashBinary) {
117        self.variance = Some(key)
118    }
119
120    /// Get the value of the variance hash
121    pub fn get_variance_key(&self) -> Option<&HashBinary> {
122        self.variance.as_ref()
123    }
124
125    /// Removes the variance from this cache key
126    pub fn remove_variance_key(&mut self) {
127        self.variance = None
128    }
129
130    /// Override the primary key hash
131    pub fn set_primary_bin_override(&mut self, key: HashBinary) {
132        self.primary_bin_override = Some(key)
133    }
134}
135
136/// Storage optimized cache key to keep in memory or in storage
137// 16 bytes + 8 bytes (+16 * u8) + user_tag.len() + 16 Bytes (Box<str>)
138#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
139pub struct CompactCacheKey {
140    pub primary: HashBinary,
141    // save 8 bytes for non-variance but waste 8 bytes for variance vs, store flat 16 bytes
142    pub variance: Option<Box<HashBinary>>,
143    pub user_tag: Box<str>, // the len should be small to keep memory usage bounded
144}
145
146impl Display for CompactCacheKey {
147    fn fmt(&self, f: &mut Formatter) -> FmtResult {
148        write!(f, "{}", hex2str(&self.primary))?;
149        if let Some(var) = &self.variance {
150            write!(f, ", variance: {}", hex2str(var.as_ref()))?;
151        }
152        write!(f, ", user_tag: {}", self.user_tag)
153    }
154}
155
156impl CacheHashKey for CompactCacheKey {
157    fn primary_bin(&self) -> HashBinary {
158        self.primary
159    }
160
161    fn variance_bin(&self) -> Option<HashBinary> {
162        self.variance.as_ref().map(|s| *s.as_ref())
163    }
164
165    fn user_tag(&self) -> &str {
166        &self.user_tag
167    }
168}
169
170/*
171 * We use blake2 hashing, which is faster and more secure, to replace md5.
172 * We have not given too much thought on whether non-crypto hash can be safely
173 * use because hashing performance is not critical.
174 * Note: we should avoid hashes like ahash which does not have consistent output
175 * across machines because it is designed purely for in memory hashtable
176*/
177
178// hash output: we use 128 bits (16 bytes) hash which will map to 32 bytes hex string
179pub(crate) type Blake2b128 = Blake2b<blake2::digest::consts::U16>;
180
181/// helper function: hash str to u8
182pub fn hash_u8(key: &str) -> u8 {
183    let mut hasher = Blake2b128::new();
184    hasher.update(key);
185    let raw = hasher.finalize();
186    raw[0]
187}
188
189/// helper function: hash str to [HashBinary]
190pub fn hash_key(key: &str) -> HashBinary {
191    let mut hasher = Blake2b128::new();
192    hasher.update(key);
193    let raw = hasher.finalize();
194    raw.into()
195}
196
197impl CacheKey {
198    fn primary_hasher(&self) -> Blake2b128 {
199        let mut hasher = Blake2b128::new();
200        hasher.update(&self.namespace);
201        hasher.update(&self.primary);
202        hasher
203    }
204
205    /// Create a default [CacheKey] from a request, which just takes it URI as the primary key.
206    pub fn default(req_header: &ReqHeader) -> Self {
207        CacheKey {
208            namespace: "".into(),
209            primary: format!("{}", req_header.uri),
210            primary_bin_override: None,
211            variance: None,
212            user_tag: "".into(),
213            extensions: Extensions::new(),
214        }
215    }
216
217    /// Create a new [CacheKey] from the given namespace, primary, and user_tag string.
218    ///
219    /// Both `namespace` and `primary` will be used for the primary hash
220    pub fn new<S1, S2, S3>(namespace: S1, primary: S2, user_tag: S3) -> Self
221    where
222        S1: Into<String>,
223        S2: Into<String>,
224        S3: Into<String>,
225    {
226        CacheKey {
227            namespace: namespace.into(),
228            primary: primary.into(),
229            primary_bin_override: None,
230            variance: None,
231            user_tag: user_tag.into(),
232            extensions: Extensions::new(),
233        }
234    }
235
236    /// Return the namespace of this key
237    pub fn namespace(&self) -> &str {
238        &self.namespace
239    }
240
241    /// Return the primary key of this key
242    pub fn primary_key(&self) -> &str {
243        &self.primary
244    }
245
246    /// Convert this key to [CompactCacheKey].
247    pub fn to_compact(&self) -> CompactCacheKey {
248        let primary = self.primary_bin();
249        CompactCacheKey {
250            primary,
251            variance: self.variance_bin().map(Box::new),
252            user_tag: self.user_tag.clone().into_boxed_str(),
253        }
254    }
255}
256
257impl CacheHashKey for CacheKey {
258    fn primary_bin(&self) -> HashBinary {
259        if let Some(primary_bin_override) = self.primary_bin_override {
260            primary_bin_override
261        } else {
262            self.primary_hasher().finalize().into()
263        }
264    }
265
266    fn variance_bin(&self) -> Option<HashBinary> {
267        self.variance
268    }
269
270    fn user_tag(&self) -> &str {
271        &self.user_tag
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_cache_key_hash() {
281        let key = CacheKey {
282            namespace: "".into(),
283            primary: "aa".into(),
284            primary_bin_override: None,
285            variance: None,
286            user_tag: "1".into(),
287            extensions: Extensions::new(),
288        };
289        let hash = key.primary();
290        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
291        assert!(key.variance().is_none());
292        assert_eq!(key.combined(), hash);
293        let compact = key.to_compact();
294        assert_eq!(compact.primary(), hash);
295        assert!(compact.variance().is_none());
296        assert_eq!(compact.combined(), hash);
297    }
298
299    #[test]
300    fn test_cache_key_hash_override() {
301        let mut key = CacheKey {
302            namespace: "".into(),
303            primary: "aa".into(),
304            primary_bin_override: str2hex("27c35e6e9373877f29e562464e46497e"),
305            variance: None,
306            user_tag: "1".into(),
307            extensions: Extensions::new(),
308        };
309        let hash = key.primary();
310        assert_eq!(hash, "27c35e6e9373877f29e562464e46497e");
311        assert!(key.variance().is_none());
312        assert_eq!(key.combined(), hash);
313        let compact = key.to_compact();
314        assert_eq!(compact.primary(), hash);
315        assert!(compact.variance().is_none());
316        assert_eq!(compact.combined(), hash);
317
318        // make sure set_primary_bin_override overrides the primary key hash correctly
319        key.set_primary_bin_override(str2hex("004174d3e75a811a5b44c46b3856f3ee").unwrap());
320        let hash = key.primary();
321        assert_eq!(hash, "004174d3e75a811a5b44c46b3856f3ee");
322        assert!(key.variance().is_none());
323        assert_eq!(key.combined(), hash);
324        let compact = key.to_compact();
325        assert_eq!(compact.primary(), hash);
326        assert!(compact.variance().is_none());
327        assert_eq!(compact.combined(), hash);
328    }
329
330    #[test]
331    fn test_cache_key_vary_hash() {
332        let key = CacheKey {
333            namespace: "".into(),
334            primary: "aa".into(),
335            primary_bin_override: None,
336            variance: Some([0u8; 16]),
337            user_tag: "1".into(),
338            extensions: Extensions::new(),
339        };
340        let hash = key.primary();
341        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
342        assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
343        assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
344        let compact = key.to_compact();
345        assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
346        assert_eq!(
347            compact.variance().unwrap(),
348            "00000000000000000000000000000000"
349        );
350        assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
351    }
352
353    #[test]
354    fn test_cache_key_vary_hash_override() {
355        let key = CacheKey {
356            namespace: "".into(),
357            primary: "saaaad".into(),
358            primary_bin_override: str2hex("ac10f2aef117729f8dad056b3059eb7e"),
359            variance: Some([0u8; 16]),
360            user_tag: "1".into(),
361            extensions: Extensions::new(),
362        };
363        let hash = key.primary();
364        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
365        assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
366        assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
367        let compact = key.to_compact();
368        assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
369        assert_eq!(
370            compact.variance().unwrap(),
371            "00000000000000000000000000000000"
372        );
373        assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
374    }
375
376    #[test]
377    fn test_hex_str() {
378        let mut key = [0; KEY_SIZE];
379        for (i, v) in key.iter_mut().enumerate() {
380            // key: [0, 1, 2, .., 15]
381            *v = i as u8;
382        }
383        let hex_str = hex2str(&key);
384        let key2 = str2hex(&hex_str).unwrap();
385        for i in 0..KEY_SIZE {
386            assert_eq!(key[i], key2[i]);
387        }
388    }
389}