Skip to main content

pingora_cache/
storage.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 backend storage abstraction
16
17use super::{CacheKey, CacheMeta};
18use crate::eviction::{CacheEntryId, CacheEntryKey, CacheEntryKeyRef};
19use crate::key::CompactCacheKey;
20use crate::trace::SpanHandle;
21
22use async_trait::async_trait;
23use pingora_error::Result;
24use std::any::Any;
25use std::fmt::{Display, Formatter, Result as FmtResult};
26
27/// The reason a purge() is called
28#[derive(Debug, Clone, Copy)]
29pub enum PurgeType {
30    // For eviction because the cache storage is full
31    Eviction,
32    // For cache invalidation
33    Invalidation,
34}
35
36/// What a purge is asked to do to the entry it targets.
37///
38/// This is separate from [`PurgeType`], which says why the purge happened rather than what it
39/// does to the entry, and from [`PurgeOutcome`], which is what storage actually did.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum PurgeAction {
42    /// Remove the entry, so the next read for its key is a miss.
43    Delete,
44    /// Keep the entry but mark it stale, so it revalidates against the origin instead of being
45    /// refetched, reusing the stored body when the origin answers 304.
46    Expire,
47}
48
49/// The entry a [`Storage::purge`] call should remove.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum PurgeTarget<'a> {
52    /// Remove storage's current entry for this logical cache key.
53    ///
54    /// This targets one entry. Storage that retains multiple generations for a logical key does
55    /// not need to remove inactive generations. Storage must not remove an entry with a different
56    /// logical key. When storage retains multiple generations, it may select which identity to
57    /// remove.
58    Active(&'a CompactCacheKey),
59    /// Remove this exact cache entry.
60    ///
61    /// Storage must match the complete identity, including any [`CacheEntryId`].
62    Exact(&'a CacheEntryKey),
63}
64
65impl Display for PurgeTarget<'_> {
66    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
67        match self {
68            Self::Active(key) => write!(f, "active entry for {key}"),
69            Self::Exact(entry) => write!(f, "{entry}"),
70        }
71    }
72}
73
74impl<'a> PurgeTarget<'a> {
75    /// Return the target's logical cache key.
76    pub fn key(self) -> &'a CompactCacheKey {
77        match self {
78            Self::Active(key) => key,
79            Self::Exact(entry) => entry.key(),
80        }
81    }
82
83    /// Return a borrowed identity for the removed entry.
84    ///
85    /// `id` supplies identity discovered while resolving a [`PurgeTarget::Active`] target. It is
86    /// ignored for a [`PurgeTarget::Exact`] target, which already contains the complete identity;
87    /// if supplied, it must match that identity.
88    pub fn removed_entry(self, id: Option<CacheEntryId>) -> CacheEntryKeyRef<'a> {
89        match self {
90            Self::Active(key) => CacheEntryKeyRef::from_entry_id(key, id),
91            Self::Exact(entry) => {
92                debug_assert!(
93                    id.is_none() || id == entry.entry_id(),
94                    "purge outcome ID {id:?} must match exact target ID {:?}",
95                    entry.entry_id()
96                );
97                entry.into()
98            }
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn exact_removed_entry_accepts_absent_or_matching_id() {
109        let id = CacheEntryId::new(42);
110        let entry = CacheEntryKey::identified(CompactCacheKey::default(), id);
111        let target = PurgeTarget::Exact(&entry);
112
113        assert_eq!(target.removed_entry(None), (&entry).into());
114        assert_eq!(target.removed_entry(Some(id)), (&entry).into());
115    }
116
117    #[cfg(debug_assertions)]
118    #[test]
119    #[should_panic(expected = "must match exact target ID")]
120    fn exact_removed_entry_rejects_mismatched_id() {
121        let entry = CacheEntryKey::identified(CompactCacheKey::default(), CacheEntryId::new(42));
122        PurgeTarget::Exact(&entry).removed_entry(Some(CacheEntryId::new(43)));
123    }
124}
125
126/// Outcome of a successful [`Storage::purge`] or [`Storage::expire`] call.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum PurgeOutcome {
129    /// Storage did not find the target entry.
130    NotFound,
131    /// Storage removed an entry, with the ID selected for an active target, if any.
132    ///
133    /// Exact targets already contain the complete identity and need not repeat their ID here.
134    Purged(Option<CacheEntryId>),
135    /// Storage kept the entry and marked it stale.
136    ///
137    /// Only [`Storage::expire`] returns this. The entry is still stored, so the eviction manager
138    /// keeps tracking it.
139    Expired,
140}
141
142/// Cache storage interface
143#[async_trait]
144pub trait Storage {
145    // TODO: shouldn't have to be static
146
147    /// Lookup the storage for the given [CacheKey].
148    async fn lookup(
149        &'static self,
150        key: &CacheKey,
151        trace: &SpanHandle,
152    ) -> Result<Option<(CacheMeta, HitHandler)>>;
153
154    /// Lookup the storage for the given [CacheKey] using a streaming write tag.
155    ///
156    /// When streaming partial writes is supported, the request that initiates the write will also
157    /// pass an optional `streaming_write_tag` so that the storage may try to find the associated
158    /// [HitHandler], for the same ongoing write.
159    ///
160    /// Therefore, when the write tag is set, the storage implementation should either return a
161    /// [HitHandler] that can be matched to that tag, or none at all. Otherwise when the storage
162    /// supports concurrent streaming writes for the same key, the calling request may receive a
163    /// different body from the one it expected.
164    ///
165    /// By default this defers to the standard `Storage::lookup` implementation.
166    async fn lookup_streaming_write(
167        &'static self,
168        key: &CacheKey,
169        _streaming_write_tag: Option<&[u8]>,
170        trace: &SpanHandle,
171    ) -> Result<Option<(CacheMeta, HitHandler)>> {
172        self.lookup(key, trace).await
173    }
174
175    /// Write the given [CacheMeta] to the storage. Return [MissHandler] to write the body later.
176    async fn get_miss_handler(
177        &'static self,
178        key: &CacheKey,
179        meta: &CacheMeta,
180        trace: &SpanHandle,
181    ) -> Result<MissHandler>;
182
183    /// Delete one cached entry for the given target.
184    ///
185    /// [`PurgeTarget::Active`] asks storage to select one active entry for a logical cache key; it
186    /// does not request removal of every retained generation. [`PurgeTarget::Exact`] identifies the
187    /// exact entry to remove.
188    ///
189    /// When resolving an active target, storage must return the storage-defined ID of the entry it
190    /// actually removed in [`PurgeOutcome::Purged`]. If [`HandleHit::entry_id`] or
191    /// [`HandleMiss::entry_id`] returns `Some`, an active purge outcome must contain that
192    /// [`CacheEntryId`]. Returning `None` would leave the identified entry tracked by the eviction
193    /// manager. Exact targets already contain the complete identity.
194    async fn purge(
195        &'static self,
196        target: PurgeTarget<'_>,
197        purge_type: PurgeType,
198        trace: &SpanHandle,
199    ) -> Result<PurgeOutcome>;
200
201    /// Mark one cached entry stale so it revalidates, keeping the entry stored.
202    ///
203    /// Targets resolve the same way as [`Storage::purge`]. Once this returns
204    /// [`PurgeOutcome::Expired`], the entry that was stored when the call ran must not be served
205    /// as a fresh hit, and its body should stay usable so a revalidation can reuse it on a 304.
206    ///
207    /// Stale is not the same as unusable. Whether a reader is served the stale body while it
208    /// revalidates is up to the entry's serve stale windows, which this does not touch.
209    /// How that is recorded is up to storage: rewriting the stored freshness and applying it on
210    /// read both satisfy the contract.
211    ///
212    /// This is deliberately not [`Storage::update_meta`]. That call needs a full [`CacheKey`] and
213    /// a [`CacheMeta`] the caller already holds from a hit, and a purge has neither. It would also
214    /// pin the recording strategy to rewriting the stored meta, turning this into a read modify
215    /// write that a concurrent revalidation can clobber.
216    ///
217    /// The guarantee covers the stored entry, not a fill already in flight. A miss handler that
218    /// commits after this returns may replace the entry with a fresh one, which is the same race
219    /// [`Storage::purge`] has and is expected to be resolved by whatever serializes writes for
220    /// that key.
221    ///
222    /// The default deletes the entry instead. Storage that cannot mark an entry stale must still
223    /// keep the next read from serving it, and deleting gives that guarantee at the cost of a
224    /// full refetch. It reports [`PurgeOutcome::Purged`] so the caller knows the entry is gone
225    /// and the eviction manager stops tracking it.
226    async fn expire(
227        &'static self,
228        target: PurgeTarget<'_>,
229        trace: &SpanHandle,
230    ) -> Result<PurgeOutcome> {
231        self.purge(target, PurgeType::Invalidation, trace).await
232    }
233
234    /// Update cache header and metadata for the already stored asset.
235    async fn update_meta(
236        &'static self,
237        key: &CacheKey,
238        meta: &CacheMeta,
239        trace: &SpanHandle,
240    ) -> Result<bool>;
241
242    /// Whether this storage backend supports reading partially written data
243    ///
244    /// This is to indicate when cache should unlock readers
245    fn support_streaming_partial_write(&self) -> bool {
246        false
247    }
248
249    /// Helper function to cast the trait object to concrete types
250    fn as_any(&self) -> &(dyn Any + Send + Sync + 'static);
251}
252
253/// Cache hit handling trait
254#[async_trait]
255pub trait HandleHit {
256    /// Read cached body
257    ///
258    /// Return `None` when no more body to read.
259    async fn read_body(&mut self) -> Result<Option<bytes::Bytes>>;
260
261    /// Finish the current cache hit
262    async fn finish(
263        self: Box<Self>, // because self is always used as a trait object
264        storage: &'static (dyn Storage + Sync),
265        key: &CacheKey,
266        trace: &SpanHandle,
267    ) -> Result<()>;
268
269    /// Whether this storage allows seeking to a certain range of body for single ranges.
270    fn can_seek(&self) -> bool {
271        false
272    }
273
274    /// Whether this storage allows seeking to a certain range of body for multipart ranges.
275    ///
276    /// By default uses the `can_seek` implementation.
277    fn can_seek_multipart(&self) -> bool {
278        self.can_seek()
279    }
280
281    /// Try to seek to a certain range of the body for single ranges.
282    ///
283    /// `end: None` means to read to the end of the body.
284    fn seek(&mut self, _start: usize, _end: Option<usize>) -> Result<()> {
285        // to prevent impl can_seek() without impl seek
286        todo!("seek() needs to be implemented")
287    }
288
289    /// Try to seek to a certain range of the body for multipart ranges.
290    ///
291    /// Works in an identical manner to `seek()`.
292    ///
293    /// `end: None` means to read to the end of the body.
294    ///
295    /// By default uses the `seek` implementation, but hit handlers may customize the
296    /// implementation specifically to anticipate multipart requests.
297    fn seek_multipart(&mut self, start: usize, end: Option<usize>) -> Result<()> {
298        // to prevent impl can_seek() without impl seek
299        self.seek(start, end)
300    }
301
302    // TODO: fn is_stream_hit()
303
304    /// Should we count this hit handler instance as an access in the eviction manager.
305    ///
306    /// Defaults to returning true to track all cache hits as accesses. Customize this if certain
307    /// hits should not affect the eviction system's view of the asset.
308    fn should_count_access(&self) -> bool {
309        true
310    }
311
312    /// Returns the weight of the current cache hit asset to report to the eviction manager.
313    ///
314    /// This allows the eviction system to initialize a weight for the asset, in case it is not
315    /// already tracking it (e.g. storage is out of sync with the eviction manager).
316    ///
317    /// Defaults to 0.
318    fn get_eviction_weight(&self) -> usize {
319        0
320    }
321
322    /// Return the identity of this entry to the eviction manager.
323    ///
324    /// Storage that identifies a stored generation beyond its
325    /// [`crate::key::CompactCacheKey`] can return that identity here so access accounting targets
326    /// the exact entry. Storage that keys eviction only on the cache key should leave this at the
327    /// default `None`.
328    ///
329    /// Storage that identifies entries must implement both this method and
330    /// [`HandleMiss::entry_id`] using the same identity scheme.
331    fn entry_id(&self) -> Option<CacheEntryId> {
332        None
333    }
334
335    /// Helper function to cast the trait object to concrete types
336    fn as_any(&self) -> &(dyn Any + Send + Sync);
337
338    /// Helper function to cast the trait object to concrete types
339    fn as_any_mut(&mut self) -> &mut (dyn Any + Send + Sync);
340}
341
342/// Hit Handler
343pub type HitHandler = Box<dyn HandleHit + Sync + Send>;
344
345/// MissFinishType
346pub enum MissFinishType {
347    /// A new asset was created with the given size.
348    Created(usize),
349    /// Appended size to existing asset, with an optional max size param.
350    Appended(usize, Option<usize>),
351}
352
353/// Cache miss handling trait
354#[async_trait]
355pub trait HandleMiss {
356    /// Write the given body to the storage
357    async fn write_body(&mut self, data: bytes::Bytes, eof: bool) -> Result<()>;
358
359    /// Finish the cache admission
360    ///
361    /// When `self` is dropped without calling this function, the storage should consider this write
362    /// failed.
363    async fn finish(
364        self: Box<Self>, // because self is always used as a trait object
365    ) -> Result<MissFinishType>;
366
367    /// Return a streaming write tag recognized by the underlying [`Storage`].
368    ///
369    /// This is an arbitrary data identifier that is used to associate this miss handler's current
370    /// write with a hit handler for the same write. This identifier will be compared by the
371    /// storage during `lookup_streaming_write`.
372    // This write tag is essentially an borrowed data blob of bytes retrieved from the miss handler
373    // and passed to storage, which means it can support strings or small data types, e.g. bytes
374    // represented by a u64.
375    // The downside with the current API is that such a data blob must be owned by the miss handler
376    // and stored in a way that permits retrieval as a byte slice (not computed on the fly).
377    // But most use cases likely only require a simple integer and may not like the overhead of a
378    // Vec/String allocation or even a Cow, though such data types can also be used here.
379    fn streaming_write_tag(&self) -> Option<&[u8]> {
380        None
381    }
382
383    /// Return the identity of the entry produced by this write to the eviction manager.
384    ///
385    /// Storage that identifies a stored generation beyond its
386    /// [`crate::key::CompactCacheKey`] should return that identity here so admission and weight
387    /// updates target the entry that storage will produce. Storage that keys eviction only on the
388    /// cache key should leave this at the default `None`.
389    ///
390    /// The value must identify the committed entry and remain valid after [`Self::finish`]. It
391    /// must not identify only temporary write state. Storage that identifies entries must implement
392    /// both this method and [`HandleHit::entry_id`] using the same identity scheme.
393    fn entry_id(&self) -> Option<CacheEntryId> {
394        None
395    }
396}
397
398/// Miss Handler
399pub type MissHandler = Box<dyn HandleMiss + Sync + Send>;
400
401pub mod streaming_write {
402    /// Portable u64 (sized) write id convenience type for use with streaming writes.
403    ///
404    /// Often an integer value is sufficient for a streaming write tag. This convenience type enables
405    /// storing such a value and functions for consistent conversion between byte sequence data types.
406    #[derive(Debug, Clone, Copy)]
407    pub struct U64WriteId([u8; 8]);
408
409    impl U64WriteId {
410        pub fn as_bytes(&self) -> &[u8] {
411            &self.0[..]
412        }
413    }
414
415    impl From<u64> for U64WriteId {
416        fn from(value: u64) -> U64WriteId {
417            U64WriteId(value.to_be_bytes())
418        }
419    }
420    impl From<U64WriteId> for u64 {
421        fn from(value: U64WriteId) -> u64 {
422            u64::from_be_bytes(value.0)
423        }
424    }
425    impl TryFrom<&[u8]> for U64WriteId {
426        type Error = std::array::TryFromSliceError;
427
428        fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
429            Ok(U64WriteId(value.try_into()?))
430        }
431    }
432
433    /// Portable u32 (sized) write id convenience type for use with streaming writes.
434    ///
435    /// Often an integer value is sufficient for a streaming write tag. This convenience type enables
436    /// storing such a value and functions for consistent conversion between byte sequence data types.
437    #[derive(Debug, Clone, Copy)]
438    pub struct U32WriteId([u8; 4]);
439
440    impl U32WriteId {
441        pub fn as_bytes(&self) -> &[u8] {
442            &self.0[..]
443        }
444    }
445
446    impl From<u32> for U32WriteId {
447        fn from(value: u32) -> U32WriteId {
448            U32WriteId(value.to_be_bytes())
449        }
450    }
451    impl From<U32WriteId> for u32 {
452        fn from(value: U32WriteId) -> u32 {
453            u32::from_be_bytes(value.0)
454        }
455    }
456    impl TryFrom<&[u8]> for U32WriteId {
457        type Error = std::array::TryFromSliceError;
458
459        fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
460            Ok(U32WriteId(value.try_into()?))
461        }
462    }
463}