Skip to main content

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