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///
97/// The primary hash is computed over the exact bytes supplied to [`CacheKey::new`].
98/// Callers that combine multiple logical components, such as a namespace and URL,
99/// must encode their boundaries unambiguously before constructing the key.
100///
101/// # Migration
102///
103/// The former `namespace` argument has been removed. Concatenating the old namespace
104/// and primary bytes preserves the legacy hash, but also preserves its ambiguous
105/// component boundaries. Switching to an unambiguous encoding changes hashes for
106/// keys with a non-empty namespace, so callers should expect a cold cache.
107#[derive(Debug, Clone)]
108pub struct CacheKey {
109    // Primary is essentially a string, except it allows invalid UTF-8 sequences.
110    // This field should be able to be hashed.
111    primary: Vec<u8>,
112    primary_bin_override: Option<HashBinary>,
113    variance: Option<HashBinary>,
114    /// An extra tag for identifying users
115    ///
116    /// For example, if the storage backend implements per user quota, this tag can be used.
117    pub user_tag: String,
118
119    /// Grab-bag for user-defined extensions. These will not be persisted to disk.
120    pub extensions: Extensions,
121}
122
123impl CacheKey {
124    /// Set the value of the variance hash
125    pub fn set_variance_key(&mut self, key: HashBinary) {
126        self.variance = Some(key)
127    }
128
129    /// Get the value of the variance hash
130    pub fn get_variance_key(&self) -> Option<&HashBinary> {
131        self.variance.as_ref()
132    }
133
134    /// Removes the variance from this cache key
135    pub fn remove_variance_key(&mut self) {
136        self.variance = None
137    }
138
139    /// Override the primary key hash
140    pub fn set_primary_bin_override(&mut self, key: HashBinary) {
141        self.primary_bin_override = Some(key)
142    }
143
144    /// Try to get primary key as UTF-8 str, if valid
145    pub fn primary_key_str(&self) -> Option<&str> {
146        std::str::from_utf8(&self.primary).ok()
147    }
148}
149
150/// Storage optimized cache key to keep in memory or in storage
151// 16 bytes + 8 bytes (+16 * u8) + user_tag.len() + 16 Bytes (Box<str>)
152#[derive(Debug, Default, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
153pub struct CompactCacheKey {
154    pub primary: HashBinary,
155    // save 8 bytes for non-variance but waste 8 bytes for variance vs, store flat 16 bytes
156    pub variance: Option<Box<HashBinary>>,
157    pub user_tag: Box<str>, // the len should be small to keep memory usage bounded
158}
159
160impl Display for CompactCacheKey {
161    fn fmt(&self, f: &mut Formatter) -> FmtResult {
162        write!(f, "{}", hex2str(&self.primary))?;
163        if let Some(var) = &self.variance {
164            write!(f, ", variance: {}", hex2str(var.as_ref()))?;
165        }
166        write!(f, ", user_tag: {}", self.user_tag)
167    }
168}
169
170impl CacheHashKey for CompactCacheKey {
171    fn primary_bin(&self) -> HashBinary {
172        self.primary
173    }
174
175    fn variance_bin(&self) -> Option<HashBinary> {
176        self.variance.as_ref().map(|s| *s.as_ref())
177    }
178
179    fn user_tag(&self) -> &str {
180        &self.user_tag
181    }
182}
183
184/*
185 * We use blake2 hashing, which is faster and more secure, to replace md5.
186 * We have not given too much thought on whether non-crypto hash can be safely
187 * use because hashing performance is not critical.
188 * Note: we should avoid hashes like ahash which does not have consistent output
189 * across machines because it is designed purely for in memory hashtable
190*/
191
192// hash output: we use 128 bits (16 bytes) hash which will map to 32 bytes hex string
193pub(crate) type Blake2b128 = Blake2b<blake2::digest::consts::U16>;
194
195/// helper function: hash str to u8
196pub fn hash_u8(key: &str) -> u8 {
197    let mut hasher = Blake2b128::new();
198    hasher.update(key);
199    let raw = hasher.finalize();
200    raw[0]
201}
202
203/// helper function: hash key (String or Bytes) to [HashBinary]
204pub fn hash_key<K: AsRef<[u8]>>(key: K) -> HashBinary {
205    let mut hasher = Blake2b128::new();
206    hasher.update(key.as_ref());
207    let raw = hasher.finalize();
208    raw.into()
209}
210
211impl CacheKey {
212    fn primary_hasher(&self) -> Blake2b128 {
213        let mut hasher = Blake2b128::new();
214        hasher.update(&self.primary);
215        hasher
216    }
217
218    /// Create a new [CacheKey] from the given `primary` key and `user_tag`.
219    ///
220    /// Only the `primary` key will be hashed to produce the primary cache hash.
221    /// If the primary contains multiple logical components, callers must frame
222    /// them unambiguously, for example by length-prefixing each component.
223    pub fn new<B, S>(primary: B, user_tag: S) -> Self
224    where
225        B: Into<Vec<u8>>,
226        S: Into<String>,
227    {
228        CacheKey {
229            primary: primary.into(),
230            primary_bin_override: None,
231            variance: None,
232            user_tag: user_tag.into(),
233            extensions: Extensions::new(),
234        }
235    }
236
237    /// Return the primary key of this key
238    pub fn primary_key(&self) -> &[u8] {
239        &self.primary[..]
240    }
241
242    /// Convert this key to [CompactCacheKey].
243    pub fn to_compact(&self) -> CompactCacheKey {
244        let primary = self.primary_bin();
245        CompactCacheKey {
246            primary,
247            variance: self.variance_bin().map(Box::new),
248            user_tag: self.user_tag.clone().into_boxed_str(),
249        }
250    }
251}
252
253impl CacheHashKey for CacheKey {
254    fn primary_bin(&self) -> HashBinary {
255        if let Some(primary_bin_override) = self.primary_bin_override {
256            primary_bin_override
257        } else {
258            self.primary_hasher().finalize().into()
259        }
260    }
261
262    fn variance_bin(&self) -> Option<HashBinary> {
263        self.variance
264    }
265
266    fn user_tag(&self) -> &str {
267        &self.user_tag
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_cache_key_hash() {
277        let key = CacheKey::new("aa", "1");
278        let hash = key.primary();
279        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
280        assert!(key.variance().is_none());
281        assert_eq!(key.combined(), hash);
282        let compact = key.to_compact();
283        assert_eq!(compact.primary(), hash);
284        assert!(compact.variance().is_none());
285        assert_eq!(compact.combined(), hash);
286    }
287
288    #[test]
289    fn test_caller_framed_primary_avoids_ambiguous_component_boundaries() {
290        let left_components = [b"tenant_a".as_slice(), b"/path".as_slice()];
291        let right_components = [b"tenant_".as_slice(), b"a/path".as_slice()];
292        let legacy_primary = left_components.concat();
293        assert_eq!(legacy_primary, right_components.concat());
294
295        fn length_prefixed(components: &[&[u8]]) -> Vec<u8> {
296            let mut primary = Vec::new();
297            for component in components {
298                primary.extend_from_slice(&(component.len() as u64).to_be_bytes());
299                primary.extend_from_slice(component);
300            }
301            primary
302        }
303
304        let left = CacheKey::new(length_prefixed(&left_components), "1");
305        let right = CacheKey::new(length_prefixed(&right_components), "1");
306        assert_ne!(left.primary_bin(), right.primary_bin());
307        assert_ne!(left.primary_bin(), hash_key(legacy_primary));
308    }
309
310    #[test]
311    fn test_raw_concatenation_preserves_legacy_hash() {
312        let mut primary = b"tenant_a".to_vec();
313        primary.extend_from_slice(b"/path");
314
315        let key = CacheKey::new(primary, "1");
316        assert_eq!(key.primary(), "6c79e74e88bacb8eb370adb7617068c8");
317    }
318
319    #[test]
320    fn test_cache_key_hash_override() {
321        let mut key = CacheKey {
322            primary: b"aa".to_vec(),
323            primary_bin_override: str2hex("27c35e6e9373877f29e562464e46497e"),
324            variance: None,
325            user_tag: "1".into(),
326            extensions: Extensions::new(),
327        };
328        let hash = key.primary();
329        assert_eq!(hash, "27c35e6e9373877f29e562464e46497e");
330        assert!(key.variance().is_none());
331        assert_eq!(key.combined(), hash);
332        let compact = key.to_compact();
333        assert_eq!(compact.primary(), hash);
334        assert!(compact.variance().is_none());
335        assert_eq!(compact.combined(), hash);
336
337        // make sure set_primary_bin_override overrides the primary key hash correctly
338        key.set_primary_bin_override(str2hex("004174d3e75a811a5b44c46b3856f3ee").unwrap());
339        let hash = key.primary();
340        assert_eq!(hash, "004174d3e75a811a5b44c46b3856f3ee");
341        assert!(key.variance().is_none());
342        assert_eq!(key.combined(), hash);
343        let compact = key.to_compact();
344        assert_eq!(compact.primary(), hash);
345        assert!(compact.variance().is_none());
346        assert_eq!(compact.combined(), hash);
347    }
348
349    #[test]
350    fn test_cache_key_vary_hash() {
351        let key = CacheKey {
352            primary: b"aa".to_vec(),
353            primary_bin_override: None,
354            variance: Some([0u8; 16]),
355            user_tag: "1".into(),
356            extensions: Extensions::new(),
357        };
358        let hash = key.primary();
359        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
360        assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
361        assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
362        let compact = key.to_compact();
363        assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
364        assert_eq!(
365            compact.variance().unwrap(),
366            "00000000000000000000000000000000"
367        );
368        assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
369    }
370
371    #[test]
372    fn test_cache_key_vary_hash_override() {
373        let key = CacheKey {
374            primary: b"saaaad".to_vec(),
375            primary_bin_override: str2hex("ac10f2aef117729f8dad056b3059eb7e"),
376            variance: Some([0u8; 16]),
377            user_tag: "1".into(),
378            extensions: Extensions::new(),
379        };
380        let hash = key.primary();
381        assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
382        assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
383        assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
384        let compact = key.to_compact();
385        assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
386        assert_eq!(
387            compact.variance().unwrap(),
388            "00000000000000000000000000000000"
389        );
390        assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
391    }
392
393    #[test]
394    fn test_hex_str() {
395        let mut key = [0; KEY_SIZE];
396        for (i, v) in key.iter_mut().enumerate() {
397            // key: [0, 1, 2, .., 15]
398            *v = i as u8;
399        }
400        let hex_str = hex2str(&key);
401        let key2 = str2hex(&hex_str).unwrap();
402        for i in 0..KEY_SIZE {
403            assert_eq!(key[i], key2[i]);
404        }
405    }
406    #[test]
407    fn test_primary_key_str_valid_utf8() {
408        let valid_utf8_key = CacheKey {
409            primary: b"/valid/path?query=1".to_vec(),
410            primary_bin_override: None,
411            variance: None,
412            user_tag: "1".into(),
413            extensions: Extensions::new(),
414        };
415
416        assert_eq!(
417            valid_utf8_key.primary_key_str(),
418            Some("/valid/path?query=1")
419        )
420    }
421
422    #[test]
423    fn test_primary_key_str_invalid_utf8() {
424        let invalid_utf8_key = CacheKey {
425            primary: vec![0x66, 0x6f, 0x6f, 0xff],
426            primary_bin_override: None,
427            variance: None,
428            user_tag: "1".into(),
429            extensions: Extensions::new(),
430        };
431
432        assert!(invalid_utf8_key.primary_key_str().is_none())
433    }
434}