Skip to main content

pingora_cache/
lib.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//! The HTTP caching layer for proxies.
16
17#![allow(clippy::new_without_default)]
18
19use http::{method::Method, request::Parts as ReqHeader, response::Parts as RespHeader};
20use key::{CacheHashKey, CompactCacheKey, HashBinary};
21use lock::WritePermit;
22use log::warn;
23use pingora_error::Result;
24use pingora_http::ResponseHeader;
25use pingora_timeout::timeout;
26use std::time::{Duration, Instant, SystemTime};
27use storage::MissFinishType;
28use strum::IntoStaticStr;
29use trace::{CacheTraceCTX, Span, Tag};
30
31pub mod admission;
32pub mod cache_control;
33pub mod eviction;
34pub mod filters;
35pub mod hashtable;
36pub mod key;
37pub mod lock;
38pub mod max_file_size;
39mod memory;
40pub mod meta;
41pub mod predictor;
42pub mod put;
43pub mod storage;
44pub mod trace;
45mod variance;
46
47use crate::max_file_size::MaxFileSizeTracker;
48use admission::{AdmissionPolicy, Decision};
49pub use eviction::{CacheEntryId, CacheEntryKey, CacheEntryKeyRef};
50pub use key::CacheKey;
51use lock::{CacheKeyLockImpl, LockStatus, LockWaitOutcome, Locked, UnusableFills, WaitOutcome};
52pub use memory::MemCache;
53pub use meta::{set_compression_dict_content, set_compression_dict_path};
54pub use meta::{CacheMeta, CacheMetaDefaults};
55pub use storage::{
56    HitHandler, MissHandler, PurgeAction, PurgeOutcome, PurgeTarget, PurgeType, Storage,
57};
58pub use variance::VarianceBuilder;
59
60pub mod prelude {}
61
62/// The state machine for http caching
63///
64/// This object is used to handle the state and transitions for HTTP caching through the life of a
65/// request.
66pub struct HttpCache {
67    phase: CachePhase,
68    // Box the rest so that a disabled HttpCache struct is small
69    inner: Option<Box<HttpCacheInner>>,
70    digest: HttpCacheDigest,
71}
72
73/// This reflects the phase of HttpCache during the lifetime of a request
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum CachePhase {
76    /// Cache disabled, with reason (NeverEnabled if never explicitly used)
77    Disabled(NoCacheReason),
78    /// Cache enabled but nothing is set yet
79    Uninit,
80    /// Cache was enabled, the request decided not to use it
81    // HttpCache.inner_enabled is kept
82    Bypass,
83    /// Awaiting the cache key to be generated
84    CacheKey,
85    /// Cache hit
86    Hit,
87    /// No cached asset is found
88    Miss,
89    /// A staled (expired) asset is found
90    Stale,
91    /// A staled (expired) asset was found, but another request is revalidating it
92    StaleUpdating,
93    /// A staled (expired) asset was found, so a fresh one was fetched
94    Expired,
95    /// A staled (expired) asset was found, and it was revalidated to be fresh
96    Revalidated,
97    /// Revalidated, but deemed uncacheable, so we do not freshen it
98    RevalidatedNoCache(NoCacheReason),
99}
100
101impl CachePhase {
102    /// Convert [CachePhase] as `str`, for logging and debugging.
103    pub fn as_str(&self) -> &'static str {
104        match self {
105            CachePhase::Disabled(_) => "disabled",
106            CachePhase::Uninit => "uninitialized",
107            CachePhase::Bypass => "bypass",
108            CachePhase::CacheKey => "key",
109            CachePhase::Hit => "hit",
110            CachePhase::Miss => "miss",
111            CachePhase::Stale => "stale",
112            CachePhase::StaleUpdating => "stale-updating",
113            CachePhase::Expired => "expired",
114            CachePhase::Revalidated => "revalidated",
115            CachePhase::RevalidatedNoCache(_) => "revalidated-nocache",
116        }
117    }
118}
119
120/// The possible reasons for not caching
121#[derive(Copy, Clone, Debug, PartialEq, Eq)]
122pub enum NoCacheReason {
123    /// Caching is not enabled to begin with
124    NeverEnabled,
125    /// Origin directives indicated this was not cacheable
126    OriginNotCache,
127    /// Response size was larger than the cache's configured maximum asset size
128    ResponseTooLarge,
129    /// Disabling caching due to unknown body size and previously exceeding maximum asset size;
130    /// the asset is otherwise cacheable, but cache needs to confirm the final size of the asset
131    /// before it can mark it as cacheable again.
132    PredictedResponseTooLarge,
133    /// Due to internal caching storage error
134    StorageError,
135    /// Due to other types of internal issues
136    InternalError,
137    /// The response may be cacheable, but this request should not fill the cache.
138    ///
139    /// This can happen when an admission policy defers an absent key, or when the cache predictor
140    /// bypassed lookup and the response cannot safely be admitted by the current request.
141    Deferred,
142    /// Due to the proxy upstream filter declining the current request from going upstream
143    DeclinedToUpstream,
144    /// Due to the upstream being unreachable or otherwise erroring during proxying
145    UpstreamError,
146    /// The writer of the cache lock sees that the request is not cacheable (Could be OriginNotCache)
147    CacheLockGiveUp,
148    /// This request waited too long for the writer of the cache lock to finish, so this request will
149    /// fetch from the origin without caching
150    CacheLockTimeout,
151    /// This request retried cache lookup too many times after waiting behind cache locks, so this
152    /// request will fetch from the origin without caching.
153    CacheLockRetryLimit,
154    /// Other custom defined reasons
155    Custom(&'static str),
156}
157
158impl NoCacheReason {
159    /// Convert [NoCacheReason] as `str`, for logging and debugging.
160    pub fn as_str(&self) -> &'static str {
161        use NoCacheReason::*;
162        match self {
163            NeverEnabled => "NeverEnabled",
164            OriginNotCache => "OriginNotCache",
165            ResponseTooLarge => "ResponseTooLarge",
166            PredictedResponseTooLarge => "PredictedResponseTooLarge",
167            StorageError => "StorageError",
168            InternalError => "InternalError",
169            Deferred => "Deferred",
170            DeclinedToUpstream => "DeclinedToUpstream",
171            UpstreamError => "UpstreamError",
172            CacheLockGiveUp => "CacheLockGiveUp",
173            CacheLockTimeout => "CacheLockTimeout",
174            CacheLockRetryLimit => "CacheLockRetryLimit",
175            Custom(s) => s,
176        }
177    }
178}
179
180/// Information collected about the caching operation that will not be cleared
181#[derive(Debug, Default)]
182pub struct HttpCacheDigest {
183    pub lock_duration: Option<Duration>,
184    // time spent in cache lookup and reading the header
185    pub lookup_duration: Option<Duration>,
186    /// Admission decision made for an absent key, if an admission policy was configured.
187    pub admission: Option<Decision>,
188    /// Set when a reader stopped waiting over a published fill it could not use.
189    /// See [`lock::UnusableFills`].
190    pub lock_abandon: Option<LockAbandon>,
191}
192
193/// A cache-lock wait abandoned over a fill the reader could not use. One value
194/// rather than two options, because the two are only ever known together.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct LockAbandon {
197    /// Why this reader cannot use the fill, from its own matched
198    /// [`lock::UnusableFill`]. The writer publishes only tokens; the reason is the
199    /// reader's, wrapped as [`NoCacheReason::Custom`].
200    pub reason: NoCacheReason,
201    /// The published token it matched.
202    pub token: u64,
203}
204
205/// Convenience function to add a duration to an optional duration
206fn add_duration_to_opt(target_opt: &mut Option<Duration>, to_add: Duration) {
207    *target_opt = Some(target_opt.map_or(to_add, |existing| existing + to_add));
208}
209
210impl HttpCacheDigest {
211    fn add_lookup_duration(&mut self, extra_lookup_duration: Duration) {
212        add_duration_to_opt(&mut self.lookup_duration, extra_lookup_duration)
213    }
214
215    fn add_lock_duration(&mut self, extra_lock_duration: Duration) {
216        add_duration_to_opt(&mut self.lock_duration, extra_lock_duration)
217    }
218}
219
220/// Response cacheable decision
221///
222///
223#[derive(Debug)]
224pub enum RespCacheable {
225    Cacheable(CacheMeta),
226    Uncacheable(NoCacheReason),
227}
228
229impl RespCacheable {
230    /// Whether it is cacheable
231    #[inline]
232    pub fn is_cacheable(&self) -> bool {
233        matches!(*self, Self::Cacheable(_))
234    }
235
236    /// Unwrap [RespCacheable] to get the [CacheMeta] stored
237    /// # Panic
238    /// Panic when this object is not cacheable. Check [Self::is_cacheable()] first.
239    pub fn unwrap_meta(self) -> CacheMeta {
240        match self {
241            Self::Cacheable(meta) => meta,
242            Self::Uncacheable(_) => panic!("expected Cacheable value"),
243        }
244    }
245}
246
247/// Indicators of which level of cache freshness logic to force apply to an asset.
248///
249/// For example, should an existing fresh asset be revalidated or re-retrieved altogether.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum ForcedFreshness {
252    /// Indicates the asset should be considered stale and revalidated, with its
253    /// stale-while-revalidate and stale-if-error windows closed so that readers wait
254    /// on the revalidation
255    ForceExpired,
256
257    /// Indicates the asset should be considered stale, leaving its
258    /// stale-while-revalidate and stale-if-error windows to decide whether the stale
259    /// body can still be served while it revalidates
260    ///
261    /// `expired_at` is when the asset went out of service, such as the timestamp a purge was
262    /// recorded at. [`CacheMeta::expire_at`] applies it and explains why it has to be that
263    /// point rather than the time of the lookup.
264    ///
265    /// `None` when the asset is out of service but the caller does not know when it went out,
266    /// which leaves the windows measured from the asset's own deadline.
267    ForceExpiredServeStale { expired_at: Option<SystemTime> },
268
269    /// Indicates the asset should be considered absent and treated like a miss
270    /// instead of a hit
271    ForceMiss,
272
273    /// Indicates the asset should be considered fresh despite possibly being stale
274    ForceFresh,
275}
276
277/// Freshness state of cache hit asset
278///
279///
280#[derive(Debug, Copy, Clone, IntoStaticStr, PartialEq, Eq)]
281#[strum(serialize_all = "snake_case")]
282pub enum HitStatus {
283    /// The asset's freshness directives indicate it has expired
284    Expired,
285
286    /// The asset was marked as expired, and should be treated as stale
287    ForceExpired,
288
289    /// The asset was marked as expired without closing its serve stale windows, so
290    /// the stale body may still be served while it revalidates
291    ForceExpiredServeStale,
292
293    /// The asset was marked as absent, and should be treated as a miss
294    ForceMiss,
295
296    /// An error occurred while processing the asset, so it should be treated as
297    /// a miss
298    FailedHitFilter,
299
300    /// The asset is not expired
301    Fresh,
302
303    /// Asset exists but is expired, forced to be a hit
304    ForceFresh,
305}
306
307impl HitStatus {
308    /// For displaying cache hit status
309    pub fn as_str(&self) -> &'static str {
310        self.into()
311    }
312
313    /// Whether cached asset can be served as fresh
314    pub fn is_fresh(&self) -> bool {
315        *self == HitStatus::Fresh || *self == HitStatus::ForceFresh
316    }
317
318    /// Check whether the hit status should be treated as a miss. A forced miss
319    /// is obviously treated as a miss. A hit-filter failure is treated as a
320    /// miss because we can't use the asset as an actual hit. If we treat it as
321    /// expired, we still might not be able to use it even if revalidation
322    /// succeeds.
323    pub fn is_treated_as_miss(self) -> bool {
324        matches!(self, HitStatus::ForceMiss | HitStatus::FailedHitFilter)
325    }
326}
327
328pub struct LockCtx {
329    pub lock: Option<Locked>,
330    pub cache_lock: &'static CacheKeyLockImpl,
331    pub wait_timeout: Option<Duration>,
332    pub max_retries: Option<usize>,
333}
334
335// Fields like storage handlers that are needed only when cache is enabled (or bypassing).
336struct HttpCacheInnerEnabled {
337    pub meta: Option<CacheMeta>,
338    // when set, even if an asset exists, it would only be considered valid after this timestamp
339    pub valid_after: Option<SystemTime>,
340    // Variance from the stale metadata before set_cache_meta() replaces it.
341    // update_variance() uses this to detect Vary family changes and reset provenance.
342    stale_meta_variance: Option<HashBinary>,
343    pub miss_handler: Option<MissHandler>,
344    pub body_reader: Option<HitHandler>,
345    pub storage: &'static (dyn storage::Storage + Sync), // static for now
346    pub eviction: Option<&'static (dyn eviction::EvictionManager + Sync)>,
347    pub admission: Option<&'static dyn AdmissionPolicy>,
348    pub lock_ctx: Option<LockCtx>,
349    pub traces: trace::CacheTraceCTX,
350}
351
352struct HttpCacheInner {
353    // Prefer adding fields to InnerEnabled if possible, these fields are released
354    // when cache is disabled.
355    // If fields are needed after cache disablement, add directly to Inner.
356    pub enabled_ctx: Option<Box<HttpCacheInnerEnabled>>,
357    pub key: Option<CacheKey>,
358    // when set, an asset will be rejected from the cache if it exceeds configured size in bytes
359    pub max_file_size_tracker: Option<MaxFileSizeTracker>,
360    pub predictor: Option<&'static (dyn predictor::CacheablePredictor + Sync)>,
361    // Why the predictor considered this key uncacheable, captured in bypass() so later
362    // phases report the reason they acted on rather than inferring one. Outlives cache
363    // disablement because it is read after the response header arrives.
364    pub predicted_uncacheable_reason: Option<NoCacheReason>,
365}
366
367#[derive(Debug, Default)]
368#[non_exhaustive]
369pub struct CacheOptionOverrides {
370    /// How long a cache lock reader should wait before giving up.
371    pub wait_timeout: Option<Duration>,
372    /// How many times a cache lock reader should retry lookup after waiting on a lock.
373    pub max_lock_retries: Option<usize>,
374}
375
376impl HttpCache {
377    /// Create a new [HttpCache].
378    ///
379    /// Caching is not enabled by default.
380    pub fn new() -> Self {
381        HttpCache {
382            phase: CachePhase::Disabled(NoCacheReason::NeverEnabled),
383            inner: None,
384            digest: HttpCacheDigest::default(),
385        }
386    }
387
388    /// Whether the cache is enabled
389    pub fn enabled(&self) -> bool {
390        !matches!(self.phase, CachePhase::Disabled(_) | CachePhase::Bypass)
391    }
392
393    /// Whether the cache is being bypassed
394    pub fn bypassing(&self) -> bool {
395        matches!(self.phase, CachePhase::Bypass)
396    }
397
398    /// Return the [CachePhase]
399    pub fn phase(&self) -> CachePhase {
400        self.phase
401    }
402
403    /// Whether anything was fetched from the upstream
404    ///
405    /// This essentially checks all possible [CachePhase] who need to contact the upstream server
406    pub fn upstream_used(&self) -> bool {
407        use CachePhase::*;
408        match self.phase {
409            Disabled(_) | Bypass | Miss | Expired | Revalidated | RevalidatedNoCache(_) => true,
410            Hit | Stale | StaleUpdating => false,
411            Uninit | CacheKey => false, // invalid states for this call, treat them as false to keep it simple
412        }
413    }
414
415    /// Check whether the backend storage is the type `T`.
416    pub fn storage_type_is<T: 'static>(&self) -> bool {
417        self.inner
418            .as_ref()
419            .and_then(|inner| {
420                inner
421                    .enabled_ctx
422                    .as_ref()
423                    .and_then(|ie| ie.storage.as_any().downcast_ref::<T>())
424            })
425            .is_some()
426    }
427
428    /// Say something about this request's cache fill, so readers coalescing behind
429    /// it that cannot use it stop waiting. See [`lock::UnusableFills`] for the
430    /// reader's side.
431    ///
432    /// No-op unless this request holds the write lock. Each call **replaces** the
433    /// last, so publish the whole set each time.
434    pub fn lock_publish_fill_tokens(&self, tokens: &[u64]) {
435        if let Some(Locked::Write(permit)) = self
436            .inner
437            .as_ref()
438            .and_then(|inner| inner.enabled_ctx.as_ref())
439            .and_then(|enabled| enabled.lock_ctx.as_ref())
440            .and_then(|lock_ctx| lock_ctx.lock.as_ref())
441        {
442            permit.publish(tokens);
443        }
444    }
445
446    /// Release the cache lock if the current request is a cache writer.
447    ///
448    /// Generally callers should prefer using `disable` when a cache lock should be released
449    /// due to an error to clear all cache context. This function is for releasing the cache lock
450    /// while still keeping the cache around for reading, e.g. when serving stale.
451    pub fn release_write_lock(&mut self, reason: NoCacheReason) {
452        use NoCacheReason::*;
453        if let Some(inner) = self.inner.as_mut() {
454            if let Some(lock_ctx) = inner
455                .enabled_ctx
456                .as_mut()
457                .and_then(|ie| ie.lock_ctx.as_mut())
458            {
459                let lock = lock_ctx.lock.take();
460                if let Some(Locked::Write(permit)) = lock {
461                    let lock_status = match reason {
462                        // let the next request try to fetch it
463                        InternalError | StorageError | Deferred | UpstreamError => {
464                            LockStatus::TransientError
465                        }
466                        // depends on why the proxy upstream filter declined the request,
467                        // for now still allow next request try to acquire to avoid thundering herd
468                        DeclinedToUpstream => LockStatus::TransientError,
469                        // no need for the lock anymore
470                        OriginNotCache | ResponseTooLarge | PredictedResponseTooLarge => {
471                            LockStatus::GiveUp
472                        }
473                        Custom(reason) => lock_ctx.cache_lock.custom_lock_status(reason),
474                        // should never happen, NeverEnabled shouldn't hold a lock
475                        NeverEnabled => panic!("NeverEnabled holds a write lock"),
476                        CacheLockGiveUp | CacheLockTimeout | CacheLockRetryLimit => {
477                            panic!("CacheLock* are for cache lock readers only")
478                        }
479                    };
480                    lock_ctx
481                        .cache_lock
482                        .release(inner.key.as_ref().unwrap(), permit, lock_status);
483                }
484            }
485        }
486    }
487
488    /// Disable caching
489    pub fn disable(&mut self, reason: NoCacheReason) {
490        // XXX: compile type enforce?
491        assert!(
492            reason != NoCacheReason::NeverEnabled,
493            "NeverEnabled not allowed as a disable reason"
494        );
495        match self.phase {
496            CachePhase::Disabled(old_reason) => {
497                // replace reason
498                if old_reason == NoCacheReason::NeverEnabled {
499                    // safeguard, don't allow replacing NeverEnabled as a reason
500                    // TODO: can be promoted to assertion once confirmed nothing is attempting this
501                    warn!("Tried to replace cache NeverEnabled with reason: {reason:?}");
502                    return;
503                }
504                self.phase = CachePhase::Disabled(reason);
505            }
506            _ => {
507                self.phase = CachePhase::Disabled(reason);
508                self.release_write_lock(reason);
509                // enabled_ctx will be cleared out
510                #[cfg_attr(not(feature = "trace"), allow(unused_mut))]
511                let mut inner_enabled = self
512                    .inner_mut()
513                    .enabled_ctx
514                    .take()
515                    .expect("could remove enabled_ctx on disable");
516                // log initial disable reason
517                inner_enabled
518                    .traces
519                    .cache_span
520                    .set_tag(|| trace::Tag::new("disable_reason", reason.as_str()));
521            }
522        }
523    }
524
525    /* The following methods panic when they are used in the wrong phase.
526     * This is better than returning errors as such panics are only caused by coding error, which
527     * should be fixed right away. Tokio runtime only crashes the current task instead of the whole
528     * program when these panics happen. */
529
530    /// Set the cache to bypass
531    ///
532    /// # Panic
533    /// This call is only allowed in [CachePhase::CacheKey] phase (before any cache lookup is performed).
534    /// Use it in any other phase will lead to panic.
535    pub fn bypass(&mut self) {
536        match self.phase {
537            CachePhase::CacheKey => {
538                // before cache lookup / found / miss
539                self.phase = CachePhase::Bypass;
540                // Record why the predictor gave up on this key while we still know.
541                // Reading it later would race with concurrent requests re-marking the key.
542                let predicted_reason = self
543                    .inner()
544                    .predictor
545                    .and_then(|predictor| predictor.predicted_uncacheable_reason(self.cache_key()));
546                self.inner_mut().predicted_uncacheable_reason = predicted_reason;
547
548                let traces = &mut self.inner_enabled_mut().traces;
549                traces
550                    .cache_span
551                    .set_tag(|| trace::Tag::new("bypassed", true));
552                if let Some(reason) = predicted_reason {
553                    traces
554                        .cache_span
555                        .set_tag(|| trace::Tag::new("bypass_reason", reason.as_str()));
556                }
557            }
558            _ => panic!("wrong phase to bypass HttpCache {:?}", self.phase),
559        }
560    }
561
562    /// Enable the cache
563    ///
564    /// - `storage`: the cache storage backend that implements [storage::Storage]
565    /// - `eviction`: optionally the eviction manager, without it, nothing will be evicted from the storage
566    /// - `predictor`: optionally a cache predictor. The cache predictor predicts whether something is likely
567    ///   to be cacheable or not. This is useful because the proxy can apply different types of optimization to
568    ///   cacheable and uncacheable requests.
569    /// - `cache_lock`: optionally a cache lock which handles concurrent lookups to the same asset. Without it
570    ///   such lookups will all be allowed to fetch the asset independently.
571    pub fn enable(
572        &mut self,
573        storage: &'static (dyn storage::Storage + Sync),
574        eviction: Option<&'static (dyn eviction::EvictionManager + Sync)>,
575        predictor: Option<&'static (dyn predictor::CacheablePredictor + Sync)>,
576        cache_lock: Option<&'static CacheKeyLockImpl>,
577        option_overrides: Option<CacheOptionOverrides>,
578    ) {
579        match self.phase {
580            CachePhase::Disabled(_) => {
581                self.phase = CachePhase::Uninit;
582
583                let wait_timeout = option_overrides
584                    .as_ref()
585                    .and_then(|overrides| overrides.wait_timeout);
586                let max_retries = option_overrides
587                    .as_ref()
588                    .and_then(|overrides| overrides.max_lock_retries);
589                let lock_ctx = cache_lock.map(|cache_lock| LockCtx {
590                    cache_lock,
591                    lock: None,
592                    wait_timeout,
593                    max_retries,
594                });
595
596                self.inner = Some(Box::new(HttpCacheInner {
597                    enabled_ctx: Some(Box::new(HttpCacheInnerEnabled {
598                        meta: None,
599                        valid_after: None,
600                        stale_meta_variance: None,
601                        miss_handler: None,
602                        body_reader: None,
603                        storage,
604                        eviction,
605                        admission: None,
606                        lock_ctx,
607                        traces: CacheTraceCTX::new(),
608                    })),
609                    key: None,
610                    max_file_size_tracker: None,
611                    predictor,
612                    predicted_uncacheable_reason: None,
613                }));
614            }
615            _ => panic!("Cannot enable already enabled HttpCache {:?}", self.phase),
616        }
617    }
618
619    /// Set the cache lock implementation.
620    /// # Panic
621    /// Must be called before a cache lock is attempted to be acquired,
622    /// i.e. in the `cache_key_callback` or `cache_hit_filter` phases.
623    pub fn set_cache_lock(
624        &mut self,
625        cache_lock: Option<&'static CacheKeyLockImpl>,
626        option_overrides: Option<CacheOptionOverrides>,
627    ) {
628        match self.phase {
629            CachePhase::Disabled(_)
630            | CachePhase::CacheKey
631            | CachePhase::Stale
632            | CachePhase::Hit => {
633                let inner_enabled = self.inner_enabled_mut();
634                if inner_enabled
635                    .lock_ctx
636                    .as_ref()
637                    .is_some_and(|ctx| ctx.lock.is_some())
638                {
639                    panic!("lock already set when resetting cache lock")
640                } else {
641                    let wait_timeout = option_overrides
642                        .as_ref()
643                        .and_then(|overrides| overrides.wait_timeout);
644                    let max_retries = option_overrides
645                        .as_ref()
646                        .and_then(|overrides| overrides.max_lock_retries);
647                    let lock_ctx = cache_lock.map(|cache_lock| LockCtx {
648                        cache_lock,
649                        lock: None,
650                        wait_timeout,
651                        max_retries,
652                    });
653                    inner_enabled.lock_ctx = lock_ctx;
654                }
655            }
656            _ => panic!("wrong phase: {:?}", self.phase),
657        }
658    }
659
660    /// Set the [`AdmissionPolicy`] used to decide whether an absent key may fill the cache.
661    ///
662    /// The policy is only consulted when storage reports a raw miss. Entries rejected
663    /// by `valid_after` filtering still follow the normal miss path.
664    ///
665    /// # Panics
666    ///
667    /// Panics after a cache lookup or fill has started.
668    pub fn set_admission_policy(&mut self, policy: &'static dyn AdmissionPolicy) {
669        match self.phase {
670            CachePhase::Uninit | CachePhase::CacheKey => {
671                self.inner_enabled_mut().admission = Some(policy);
672            }
673            _ => panic!("wrong phase to set admission policy: {:?}", self.phase),
674        }
675    }
676
677    // Enable distributed tracing
678    pub fn enable_tracing(&mut self, parent_span: trace::Span) {
679        if let Some(inner_enabled) = self.inner.as_mut().and_then(|i| i.enabled_ctx.as_mut()) {
680            inner_enabled.traces.enable(parent_span);
681        }
682    }
683
684    // Get the cache parent tracing span
685    pub fn get_cache_span(&self) -> Option<trace::SpanHandle> {
686        self.inner
687            .as_ref()
688            .and_then(|i| i.enabled_ctx.as_ref().map(|ie| ie.traces.get_cache_span()))
689    }
690
691    // Get the cache `miss` tracing span
692    pub fn get_miss_span(&self) -> Option<trace::SpanHandle> {
693        self.inner
694            .as_ref()
695            .and_then(|i| i.enabled_ctx.as_ref().map(|ie| ie.traces.get_miss_span()))
696    }
697
698    // Get the cache `hit` tracing span
699    pub fn get_hit_span(&self) -> Option<trace::SpanHandle> {
700        self.inner
701            .as_ref()
702            .and_then(|i| i.enabled_ctx.as_ref().map(|ie| ie.traces.get_hit_span()))
703    }
704
705    // shortcut to access inner fields, panic if phase is disabled
706    #[inline]
707    fn inner_enabled_mut(&mut self) -> &mut HttpCacheInnerEnabled {
708        self.inner.as_mut().unwrap().enabled_ctx.as_mut().unwrap()
709    }
710
711    #[inline]
712    fn inner_enabled(&self) -> &HttpCacheInnerEnabled {
713        self.inner.as_ref().unwrap().enabled_ctx.as_ref().unwrap()
714    }
715
716    // shortcut to access inner fields, panic if cache was never enabled
717    #[inline]
718    fn inner_mut(&mut self) -> &mut HttpCacheInner {
719        self.inner.as_mut().unwrap()
720    }
721
722    #[inline]
723    fn inner(&self) -> &HttpCacheInner {
724        self.inner.as_ref().unwrap()
725    }
726
727    /// Set the cache key
728    /// # Panic
729    /// Cache key is only allowed to be set in its own phase. Set it in other phases will cause panic.
730    pub fn set_cache_key(&mut self, key: CacheKey) {
731        match self.phase {
732            CachePhase::Uninit | CachePhase::CacheKey => {
733                self.phase = CachePhase::CacheKey;
734                self.inner_mut().key = Some(key);
735            }
736            _ => panic!("wrong phase {:?}", self.phase),
737        }
738    }
739
740    /// Return the cache key used for asset lookup
741    /// # Panic
742    /// Can only be called after the cache key is set and the cache is not disabled. Panic otherwise.
743    pub fn cache_key(&self) -> &CacheKey {
744        match self.phase {
745            CachePhase::Disabled(NoCacheReason::NeverEnabled) | CachePhase::Uninit => {
746                panic!("wrong phase {:?}", self.phase)
747            }
748            _ => self
749                .inner()
750                .key
751                .as_ref()
752                .expect("cache key should be set (set_cache_key not called?)"),
753        }
754    }
755
756    /// Return the max size allowed to be cached.
757    pub fn max_file_size_bytes(&self) -> Option<usize> {
758        assert!(
759            !matches!(
760                self.phase,
761                CachePhase::Disabled(NoCacheReason::NeverEnabled)
762            ),
763            "tried to access max file size bytes when cache never enabled"
764        );
765        self.inner()
766            .max_file_size_tracker
767            .as_ref()
768            .map(|t| t.max_file_size_bytes())
769    }
770
771    /// Set the maximum response _body_ size in bytes that will be admitted to the cache.
772    ///
773    /// Response header size should not contribute to the max file size.
774    ///
775    /// To track body bytes, call `track_bytes_for_max_file_size`.
776    pub fn set_max_file_size_bytes(&mut self, max_file_size_bytes: usize) {
777        match self.phase {
778            CachePhase::Disabled(_) => panic!("wrong phase {:?}", self.phase),
779            _ => {
780                self.inner_mut().max_file_size_tracker =
781                    Some(MaxFileSizeTracker::new(max_file_size_bytes));
782            }
783        }
784    }
785
786    /// Record body bytes for the max file size tracker.
787    ///
788    /// The `bytes_len` input contributes to a cumulative body byte tracker.
789    ///
790    /// Once the cumulative body bytes exceeds the maximum allowable cache file size (as configured
791    /// by `set_max_file_size_bytes`), then the return value will be false.
792    ///
793    /// Else the return value is true as long as the max file size is not exceeded.
794    /// If max file size was not configured, the return value is always true.
795    pub fn track_body_bytes_for_max_file_size(&mut self, bytes_len: usize) -> bool {
796        // This is intended to be callable when cache has already been disabled,
797        // so that we can re-mark an asset as cacheable if the body size is under limits.
798        assert!(
799            !matches!(
800                self.phase,
801                CachePhase::Disabled(NoCacheReason::NeverEnabled)
802            ),
803            "tried to access max file size bytes when cache never enabled"
804        );
805        self.inner_mut()
806            .max_file_size_tracker
807            .as_mut()
808            .is_none_or(|t| t.add_body_bytes(bytes_len))
809    }
810
811    /// Check if the max file size has been exceeded according to max file size tracker.
812    ///
813    /// Return true if max file size was exceeded.
814    pub fn exceeded_max_file_size(&self) -> bool {
815        assert!(
816            !matches!(
817                self.phase,
818                CachePhase::Disabled(NoCacheReason::NeverEnabled)
819            ),
820            "tried to access max file size bytes when cache never enabled"
821        );
822        self.inner()
823            .max_file_size_tracker
824            .as_ref()
825            .is_some_and(|t| !t.allow_caching())
826    }
827
828    /// Set that cache is found in cache storage.
829    ///
830    /// This function is called after [Self::cache_lookup()] which returns the [CacheMeta] and
831    /// [HitHandler].
832    ///
833    /// The `hit_status` enum allows the caller to force expire assets.
834    pub fn cache_found(&mut self, meta: CacheMeta, hit_handler: HitHandler, hit_status: HitStatus) {
835        // Stale allowed because of cache lock and then retry
836        if !matches!(self.phase, CachePhase::CacheKey | CachePhase::Stale) {
837            panic!("wrong phase {:?}", self.phase)
838        }
839
840        self.phase = match hit_status {
841            HitStatus::Fresh | HitStatus::ForceFresh => CachePhase::Hit,
842            HitStatus::Expired | HitStatus::ForceExpired | HitStatus::ForceExpiredServeStale => {
843                CachePhase::Stale
844            }
845            HitStatus::FailedHitFilter | HitStatus::ForceMiss => self.phase,
846        };
847
848        let phase = self.phase;
849        let inner = self.inner_mut();
850
851        let key = inner.key.as_ref().expect("key must be set on hit");
852        let inner_enabled = inner
853            .enabled_ctx
854            .as_mut()
855            .expect("cache_found must be called while cache enabled");
856
857        // The cache lock might not be set for stale hit or hits treated as
858        // misses, so we need to initialize it here
859        let stale = phase == CachePhase::Stale;
860        if stale || hit_status.is_treated_as_miss() {
861            if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
862                lock_ctx.lock = Some(lock_ctx.cache_lock.lock(key, stale));
863            }
864        }
865
866        if hit_status.is_treated_as_miss() {
867            // Clear the body and meta for hits that are treated as misses
868            inner_enabled.body_reader = None;
869            inner_enabled.meta = None;
870        } else {
871            // Set the metadata appropriately for legit hits
872            inner_enabled.traces.start_hit_span(phase, hit_status);
873            inner_enabled.traces.log_meta_in_hit_span(&meta);
874            if let Some(eviction) = inner_enabled.eviction {
875                let cache_key = key.to_compact();
876                if hit_handler.should_count_access() {
877                    let size = hit_handler.get_eviction_weight();
878                    let entry_key =
879                        eviction::CacheEntryKey::from_entry_id(cache_key, hit_handler.entry_id());
880                    eviction.access(&entry_key, size, meta.0.internal.fresh_until);
881                }
882            }
883            inner_enabled.meta = Some(meta);
884            inner_enabled.body_reader = Some(hit_handler);
885        }
886    }
887
888    /// Mark `self` to be cache miss.
889    ///
890    /// This function is called after [Self::cache_lookup()] finds nothing or the caller decides
891    /// not to use the assets found.
892    /// # Panic
893    /// Panic in other phases.
894    pub fn cache_miss(&mut self) {
895        match self.phase {
896            // from CacheKey: set state to miss during cache lookup
897            // from Bypass: response became cacheable, set state to miss to cache
898            // from Stale: waited for cache lock, then retried and found asset was gone
899            CachePhase::CacheKey | CachePhase::Bypass | CachePhase::Stale => {
900                self.phase = CachePhase::Miss;
901                // It's possible that we've set the meta on lookup and have come back around
902                // here after not being able to acquire the cache lock, and our item has since
903                // purged or expired. We should be sure that the meta is not set in this case
904                // as there shouldn't be a meta set for cache misses.
905                let inner_enabled = self.inner_enabled_mut();
906                inner_enabled.meta = None;
907                inner_enabled.stale_meta_variance = None;
908                inner_enabled.traces.start_miss_span();
909            }
910            _ => panic!("wrong phase {:?}", self.phase),
911        }
912    }
913
914    /// Return the [HitHandler]
915    /// # Panic
916    /// Call this after [Self::cache_found()], panic in other phases.
917    pub fn hit_handler(&mut self) -> &mut HitHandler {
918        match self.phase {
919            CachePhase::Hit
920            | CachePhase::Stale
921            | CachePhase::StaleUpdating
922            | CachePhase::Revalidated
923            | CachePhase::RevalidatedNoCache(_) => {
924                self.inner_enabled_mut().body_reader.as_mut().unwrap()
925            }
926            _ => panic!("wrong phase {:?}", self.phase),
927        }
928    }
929
930    /// Return the body reader during a cache admission (miss/expired) which decouples the downstream
931    /// read and upstream cache write
932    pub fn miss_body_reader(&mut self) -> Option<&mut HitHandler> {
933        match self.phase {
934            CachePhase::Miss | CachePhase::Expired => {
935                let inner_enabled = self.inner_enabled_mut();
936                if inner_enabled.storage.support_streaming_partial_write() {
937                    inner_enabled.body_reader.as_mut()
938                } else {
939                    // body_reader could be set even when the storage doesn't support streaming
940                    // Expired cache would have the reader set.
941                    None
942                }
943            }
944            _ => None,
945        }
946    }
947
948    /// Return whether the underlying storage backend supports streaming partial write.
949    ///
950    /// Returns None if cache is not enabled.
951    pub fn support_streaming_partial_write(&self) -> Option<bool> {
952        self.inner.as_ref().and_then(|inner| {
953            inner
954                .enabled_ctx
955                .as_ref()
956                .map(|c| c.storage.support_streaming_partial_write())
957        })
958    }
959
960    /// Call this when cache hit is fully read.
961    ///
962    /// This call will release resource if any and log the timing in tracing if set.
963    /// # Panic
964    /// Panic in phases where there is no cache hit.
965    pub async fn finish_hit_handler(&mut self) -> Result<()> {
966        match self.phase {
967            CachePhase::Hit
968            | CachePhase::Miss
969            | CachePhase::Expired
970            | CachePhase::Stale
971            | CachePhase::StaleUpdating
972            | CachePhase::Revalidated
973            | CachePhase::RevalidatedNoCache(_) => {
974                let inner = self.inner_mut();
975                let inner_enabled = inner.enabled_ctx.as_mut().expect("cache enabled");
976                if inner_enabled.body_reader.is_none() {
977                    // already finished, we allow calling this function more than once
978                    return Ok(());
979                }
980                let body_reader = inner_enabled.body_reader.take().unwrap();
981                let key = inner.key.as_ref().unwrap();
982                let result = body_reader
983                    .finish(
984                        inner_enabled.storage,
985                        key,
986                        &inner_enabled.traces.hit_span.handle(),
987                    )
988                    .await;
989                inner_enabled.traces.finish_hit_span();
990                result
991            }
992            _ => panic!("wrong phase {:?}", self.phase),
993        }
994    }
995
996    /// Set the [MissHandler] according to cache_key and meta, can only call once
997    pub async fn set_miss_handler(&mut self) -> Result<()> {
998        match self.phase {
999            // set_miss_handler() needs to be called after set_cache_meta() (which change Stale to Expire).
1000            // This is an artificial rule to enforce the state transitions
1001            CachePhase::Miss | CachePhase::Expired => {
1002                let inner = self.inner_mut();
1003                let inner_enabled = inner
1004                    .enabled_ctx
1005                    .as_mut()
1006                    .expect("cache enabled on miss and expired");
1007                if inner_enabled.miss_handler.is_some() {
1008                    panic!("write handler is already set")
1009                }
1010                let meta = inner_enabled.meta.as_ref().unwrap();
1011                let key = inner.key.as_ref().unwrap();
1012                let miss_handler = inner_enabled
1013                    .storage
1014                    .get_miss_handler(key, meta, &inner_enabled.traces.get_miss_span())
1015                    .await?;
1016
1017                inner_enabled.miss_handler = Some(miss_handler);
1018
1019                if inner_enabled.storage.support_streaming_partial_write() {
1020                    // If a reader can access partial write, the cache lock can be released here
1021                    // to let readers start reading the body.
1022                    if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1023                        let lock = lock_ctx.lock.take();
1024                        if let Some(Locked::Write(permit)) = lock {
1025                            lock_ctx.cache_lock.release(key, permit, LockStatus::Done);
1026                        }
1027                    }
1028                    // Downstream read and upstream write can be decoupled
1029                    let body_reader = inner_enabled
1030                        .storage
1031                        .lookup_streaming_write(
1032                            key,
1033                            inner_enabled
1034                                .miss_handler
1035                                .as_ref()
1036                                .expect("miss handler already set")
1037                                .streaming_write_tag(),
1038                            &inner_enabled.traces.get_miss_span(),
1039                        )
1040                        .await?;
1041
1042                    if let Some((_meta, body_reader)) = body_reader {
1043                        inner_enabled.body_reader = Some(body_reader);
1044                    } else {
1045                        // body_reader should exist now because streaming_partial_write is to support it
1046                        panic!("unable to get body_reader for {:?}", meta);
1047                    }
1048                }
1049                Ok(())
1050            }
1051            _ => panic!("wrong phase {:?}", self.phase),
1052        }
1053    }
1054
1055    /// Return the [MissHandler] to write the response body to cache.
1056    ///
1057    /// `None`: the handler has not been set or already finished
1058    pub fn miss_handler(&mut self) -> Option<&mut MissHandler> {
1059        match self.phase {
1060            CachePhase::Miss | CachePhase::Expired => {
1061                self.inner_enabled_mut().miss_handler.as_mut()
1062            }
1063            _ => panic!("wrong phase {:?}", self.phase),
1064        }
1065    }
1066
1067    /// Finish cache admission
1068    ///
1069    /// If [self] is dropped without calling this, the cache admission is considered incomplete and
1070    /// should be cleaned up.
1071    ///
1072    /// This call will also trigger eviction if set.
1073    pub async fn finish_miss_handler(&mut self) -> Result<()> {
1074        match self.phase {
1075            CachePhase::Miss | CachePhase::Expired => {
1076                let inner = self.inner_mut();
1077                let inner_enabled = inner
1078                    .enabled_ctx
1079                    .as_mut()
1080                    .expect("cache enabled on miss and expired");
1081                let Some(miss_handler) = inner_enabled.miss_handler.take() else {
1082                    // already finished, we allow calling this function more than once
1083                    return Ok(());
1084                };
1085                // Save the entry ID before `finish` consumes the miss handler.
1086                let entry_id = miss_handler.entry_id();
1087                let finish_result = miss_handler.finish().await;
1088                let key = inner
1089                    .key
1090                    .as_ref()
1091                    .expect("key set by miss or expired phase");
1092                if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1093                    let lock = lock_ctx.lock.take();
1094                    if let Some(Locked::Write(permit)) = lock {
1095                        // no need to call r.unlock() because release() will call it
1096                        // r is a guard to make sure the lock is unlocked when this request is dropped
1097                        let lock_status = if finish_result.is_ok() {
1098                            LockStatus::Done
1099                        } else {
1100                            LockStatus::TransientError
1101                        };
1102                        lock_ctx.cache_lock.release(key, permit, lock_status);
1103                    }
1104                }
1105                let size = match finish_result {
1106                    Ok(size) => size,
1107                    Err(e) => {
1108                        inner_enabled.traces.finish_miss_span();
1109                        return Err(e);
1110                    }
1111                };
1112                if let Some(eviction) = inner_enabled.eviction {
1113                    let cache_key = key.to_compact();
1114                    let meta = inner_enabled.meta.as_ref().unwrap();
1115                    let entry_key = eviction::CacheEntryKey::from_entry_id(cache_key, entry_id);
1116                    let evicted = match size {
1117                        MissFinishType::Created(size) => {
1118                            eviction.admit(entry_key, size, meta.0.internal.fresh_until)
1119                        }
1120                        MissFinishType::Appended(size, max_size) => {
1121                            eviction.increment_weight(&entry_key, size, max_size)
1122                        }
1123                    };
1124                    // actual eviction can be done async
1125                    let span = inner_enabled.traces.child("eviction");
1126                    let handle = span.handle();
1127                    let storage = inner_enabled.storage;
1128                    tokio::task::spawn(async move {
1129                        for item in evicted {
1130                            let target = storage::PurgeTarget::Exact(&item);
1131                            if let Err(e) =
1132                                storage.purge(target, PurgeType::Eviction, &handle).await
1133                            {
1134                                warn!(
1135                                    "Failed to purge {target} during eviction for finish miss handler: {e}"
1136                                );
1137                            }
1138                        }
1139                    });
1140                }
1141                inner_enabled.traces.finish_miss_span();
1142                Ok(())
1143            }
1144            _ => panic!("wrong phase {:?}", self.phase),
1145        }
1146    }
1147
1148    /// Set the [CacheMeta] of the cache
1149    ///
1150    /// # Panics
1151    ///
1152    /// Panics unless called in [CachePhase::Miss] or [CachePhase::Stale]. In stale phase, the
1153    /// stale metadata must still be present.
1154    pub fn set_cache_meta(&mut self, mut meta: CacheMeta) {
1155        match self.phase {
1156            // TODO: store the staled meta somewhere else for future use?
1157            CachePhase::Stale => {
1158                let inner_enabled = self.inner_enabled_mut();
1159                let old_meta = inner_enabled
1160                    .meta
1161                    .as_ref()
1162                    .expect("stale phase has cache meta");
1163                inner_enabled.stale_meta_variance = old_meta.variance();
1164                meta.set_provenance(old_meta.provenance());
1165                // TODO: have a separate expired span?
1166                inner_enabled.traces.log_meta_in_miss_span(&meta);
1167                inner_enabled.meta = Some(meta);
1168            }
1169            CachePhase::Miss => {
1170                let inner_enabled = self.inner_enabled_mut();
1171                inner_enabled.stale_meta_variance = None;
1172                // TODO: have a separate expired span?
1173                inner_enabled.traces.log_meta_in_miss_span(&meta);
1174                inner_enabled.meta = Some(meta);
1175            }
1176            _ => panic!("wrong phase {:?}", self.phase),
1177        }
1178        if self.phase == CachePhase::Stale {
1179            self.phase = CachePhase::Expired;
1180        }
1181    }
1182
1183    /// Set the [CacheMeta] of the cache after revalidation.
1184    ///
1185    /// Certain info such as the original cache admission time will be preserved. Others will
1186    /// be replaced by the input `meta`.
1187    pub async fn revalidate_cache_meta(&mut self, mut meta: CacheMeta) -> Result<bool> {
1188        let result = match self.phase {
1189            CachePhase::Stale => {
1190                let inner = self.inner_mut();
1191                let inner_enabled = inner
1192                    .enabled_ctx
1193                    .as_mut()
1194                    .expect("stale phase has cache enabled");
1195                // TODO: we should keep old meta in place, just use new one to update it
1196                // that requires cacheable_filter to take a mut header and just return InternalMeta
1197
1198                // update new meta with old meta's created time
1199                let old_meta = inner_enabled.meta.take().unwrap();
1200                let created = old_meta.0.internal.created;
1201                let provenance = old_meta.provenance();
1202                meta.0.internal.created = created;
1203                meta.set_provenance(provenance);
1204                // meta.internal.updated was already set to new meta's `created`,
1205                // no need to set `updated` here
1206                // Merge old extensions with new ones. New exts take precedence if they conflict.
1207                let mut extensions = old_meta.0.extensions;
1208                extensions.extend(meta.0.extensions);
1209                meta.0.extensions = extensions;
1210                inner_enabled.stale_meta_variance = None;
1211
1212                inner_enabled.meta.replace(meta);
1213
1214                #[cfg_attr(not(feature = "trace"), allow(unused_mut))]
1215                let mut span = inner_enabled.traces.child("update_meta");
1216                let result = inner_enabled
1217                    .storage
1218                    .update_meta(
1219                        inner.key.as_ref().unwrap(),
1220                        inner_enabled.meta.as_ref().unwrap(),
1221                        &span.handle(),
1222                    )
1223                    .await;
1224                span.set_tag(|| trace::Tag::new("updated", result.is_ok()));
1225
1226                // regardless of result, release the cache lock
1227                if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1228                    let lock = lock_ctx.lock.take();
1229                    if let Some(Locked::Write(permit)) = lock {
1230                        lock_ctx.cache_lock.release(
1231                            inner.key.as_ref().expect("key set by stale phase"),
1232                            permit,
1233                            LockStatus::Done,
1234                        );
1235                    }
1236                }
1237
1238                result
1239            }
1240            _ => panic!("wrong phase {:?}", self.phase),
1241        };
1242        self.phase = CachePhase::Revalidated;
1243        result
1244    }
1245
1246    /// After a successful revalidation, update certain headers for the cached asset
1247    /// such as `Etag` with the fresh response header `resp`.
1248    pub fn revalidate_merge_header(&mut self, resp: &RespHeader) -> ResponseHeader {
1249        match self.phase {
1250            CachePhase::Stale => {
1251                /*
1252                 * https://datatracker.ietf.org/doc/html/rfc9110#section-15.4.5
1253                 * 304 response MUST generate ... would have been sent in a 200 ...
1254                 * - Content-Location, Date, ETag, and Vary
1255                 * - Cache-Control and Expires...
1256                 */
1257                let mut old_header = self.inner_enabled().meta.as_ref().unwrap().0.header.clone();
1258                let mut clone_header = |header_name: &'static str| {
1259                    for (i, value) in resp.headers.get_all(header_name).iter().enumerate() {
1260                        if i == 0 {
1261                            old_header
1262                                .insert_header(header_name, value)
1263                                .expect("can add valid header");
1264                        } else {
1265                            old_header
1266                                .append_header(header_name, value)
1267                                .expect("can add valid header");
1268                        }
1269                    }
1270                };
1271                clone_header("cache-control");
1272                clone_header("expires");
1273                clone_header("cache-tag");
1274                clone_header("cdn-cache-control");
1275                clone_header("etag");
1276                // https://datatracker.ietf.org/doc/html/rfc9111#section-4.3.4
1277                // "...cache MUST update its header fields with the header fields provided in the 304..."
1278                // But if the Vary header changes, the cached response may no longer match the
1279                // incoming request.
1280                //
1281                // For simplicity, ignore changing Vary in revalidation for now.
1282                // TODO: if we support vary during revalidation, there are a few edge cases to
1283                // consider (what if Vary header appears/disappears/changes)?
1284                //
1285                // clone_header("vary");
1286                old_header
1287            }
1288            _ => panic!("wrong phase {:?}", self.phase),
1289        }
1290    }
1291
1292    /// Mark this asset uncacheable after revalidation
1293    pub fn revalidate_uncacheable(&mut self, header: ResponseHeader, reason: NoCacheReason) {
1294        match self.phase {
1295            CachePhase::Stale => {
1296                // replace cache meta header
1297                self.inner_enabled_mut().meta.as_mut().unwrap().0.header = header;
1298                // upstream request done, release write lock
1299                self.release_write_lock(reason);
1300            }
1301            _ => panic!("wrong phase {:?}", self.phase),
1302        }
1303        self.phase = CachePhase::RevalidatedNoCache(reason);
1304        // TODO: remove this asset from cache once finished?
1305    }
1306
1307    /// Mark this asset as stale, but being updated separately from this request.
1308    pub fn set_stale_updating(&mut self) {
1309        match self.phase {
1310            CachePhase::Stale => self.phase = CachePhase::StaleUpdating,
1311            _ => panic!("wrong phase {:?}", self.phase),
1312        }
1313    }
1314
1315    /// Update the variance of the [CacheMeta].
1316    ///
1317    /// Note that this process may change the lookup `key`, and eventually (when the asset is
1318    /// written to storage) invalidate other cached variants under the same primary key as the
1319    /// current asset.
1320    pub fn update_variance(&mut self, variance: Option<HashBinary>) {
1321        // If this is a cache miss, we will simply update the variance in the meta.
1322        //
1323        // If this is an expired response, we will have to consider a few cases:
1324        //
1325        // **Case 1**: Variance was absent, but caller sets it now.
1326        // We will just insert it into the meta. The current asset becomes the primary variant.
1327        // Because the current location of the asset is already the primary variant, the lookup key
1328        // does not need to change. If this is an expired response, this is a new Vary family, so
1329        // provenance is reset to the refreshed metadata's created timestamp.
1330        //
1331        // **Case 2**: Variance was present, but it changed or was removed.
1332        // We want the current asset to take over the primary slot, in order to invalidate all
1333        // other variants derived under the old Vary. For expired responses, provenance is reset
1334        // to the refreshed metadata's created timestamp.
1335        //
1336        // **Case 3**: Variance did not change.
1337        // Nothing needs to happen.
1338        //
1339        // These provenance updates do not provide ordering on their own. Writers need a cache lock
1340        // to avoid racing each other. A purge can still race with a stale refresh: whichever observes
1341        // or writes storage last determines whether old provenance is carried forward or replaced.
1342        let phase = self.phase;
1343        let inner = match phase {
1344            CachePhase::Miss | CachePhase::Expired => self.inner_mut(),
1345            _ => panic!("wrong phase {:?}", self.phase),
1346        };
1347        let inner_enabled = inner
1348            .enabled_ctx
1349            .as_mut()
1350            .expect("cache enabled on miss and expired");
1351        let old_key_variance = inner.key.as_ref().unwrap().get_variance_key().copied();
1352        let stale_meta_variance = if phase == CachePhase::Expired {
1353            inner_enabled.stale_meta_variance.take()
1354        } else {
1355            inner_enabled.stale_meta_variance = None;
1356            None
1357        };
1358        let reset_provenance_to_created = if phase == CachePhase::Expired {
1359            match old_key_variance {
1360                Some(old_variance) => Some(old_variance) != variance,
1361                None => stale_meta_variance != variance,
1362            }
1363        } else {
1364            false
1365        };
1366
1367        // Update the variance in the meta
1368        if let Some(variance_hash) = variance.as_ref() {
1369            inner_enabled
1370                .meta
1371                .as_mut()
1372                .unwrap()
1373                .set_variance_key(*variance_hash);
1374        } else {
1375            inner_enabled.meta.as_mut().unwrap().remove_variance();
1376        }
1377        if reset_provenance_to_created {
1378            inner_enabled
1379                .meta
1380                .as_mut()
1381                .unwrap()
1382                .reset_provenance_to_created();
1383        }
1384
1385        // Change the lookup `key` if necessary, in order to admit asset into the primary slot
1386        // instead of the secondary slot.
1387        let key = inner.key.as_ref().unwrap();
1388        if let Some(old_variance) = old_key_variance {
1389            // This is a secondary variant slot.
1390            if Some(old_variance) != variance {
1391                // This new variance does not match the variance in the cache key we used to look
1392                // up this asset.
1393                // Drop the cache lock to avoid leaving a dangling lock
1394                // (because we locked with the old cache key for the secondary slot)
1395                // TODO: maybe we should try to signal waiting readers to compete for the primary key
1396                // lock instead? we will not be modifying this secondary slot so it's not actually
1397                // ready for readers
1398                if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1399                    if let Some(Locked::Write(permit)) = lock_ctx.lock.take() {
1400                        lock_ctx.cache_lock.release(key, permit, LockStatus::Done);
1401                    }
1402                }
1403                // Remove the `variance` from the `key`, so that we admit this asset into the
1404                // primary slot. (`key` is used to tell storage where to write the data.)
1405                inner.key.as_mut().unwrap().remove_variance_key();
1406            }
1407        }
1408    }
1409
1410    /// Return the [CacheMeta] of this asset
1411    ///
1412    /// # Panic
1413    /// Panic in phases which has no cache meta.
1414    pub fn cache_meta(&self) -> &CacheMeta {
1415        match self.phase {
1416            // TODO: allow in Bypass phase?
1417            CachePhase::Stale
1418            | CachePhase::StaleUpdating
1419            | CachePhase::Expired
1420            | CachePhase::Hit
1421            | CachePhase::Revalidated
1422            | CachePhase::RevalidatedNoCache(_) => self.inner_enabled().meta.as_ref().unwrap(),
1423            CachePhase::Miss => {
1424                // this is the async body read case, safe because body_reader is only set
1425                // after meta is retrieved
1426                if self.inner_enabled().body_reader.is_some() {
1427                    self.inner_enabled().meta.as_ref().unwrap()
1428                } else {
1429                    panic!("wrong phase {:?}", self.phase);
1430                }
1431            }
1432
1433            _ => panic!("wrong phase {:?}", self.phase),
1434        }
1435    }
1436
1437    /// Return the [CacheMeta] of this asset if any
1438    ///
1439    /// Different from [Self::cache_meta()], this function is allowed to be called in
1440    /// any phase and will not panic due to a wrong phase. It returns the cache meta in
1441    /// the phases where one may be set ([CachePhase::Miss], [CachePhase::Stale],
1442    /// [CachePhase::StaleUpdating], [CachePhase::Expired], [CachePhase::Hit],
1443    /// [CachePhase::Revalidated], and [CachePhase::RevalidatedNoCache]); in all other
1444    /// phases it returns `None` because no cache meta can exist.
1445    pub fn maybe_cache_meta(&self) -> Option<&CacheMeta> {
1446        match self.phase {
1447            CachePhase::Miss
1448            | CachePhase::Stale
1449            | CachePhase::StaleUpdating
1450            | CachePhase::Expired
1451            | CachePhase::Hit
1452            | CachePhase::Revalidated
1453            | CachePhase::RevalidatedNoCache(_) => self.inner_enabled().meta.as_ref(),
1454            _ => None,
1455        }
1456    }
1457
1458    /// Return the [`CacheKey`] of this asset if any.
1459    ///
1460    /// This is allowed to be called in any phase. If the cache key callback was not called,
1461    /// this will return None.
1462    pub fn maybe_cache_key(&self) -> Option<&CacheKey> {
1463        (!matches!(
1464            self.phase(),
1465            CachePhase::Disabled(NoCacheReason::NeverEnabled) | CachePhase::Uninit
1466        ))
1467        .then(|| self.cache_key())
1468    }
1469
1470    /// Perform the cache lookup from the given cache storage with the given cache key
1471    ///
1472    /// A cache hit will return [CacheMeta] which contains the header and meta info about
1473    /// the cache as well as a [HitHandler] to read the cache hit body.
1474    ///
1475    /// When an admission policy defers a raw storage miss, this returns `Ok(None)` and disables
1476    /// caching with [`NoCacheReason::Deferred`]. Callers must check [`Self::enabled()`] before
1477    /// calling [`Self::cache_miss()`].
1478    ///
1479    /// Admission is observed at most once per [`HttpCache`], on an initial
1480    /// [`CachePhase::CacheKey`] raw storage miss. Retried lookups and stale refills reuse the
1481    /// existing admission outcome or proceed without another observation.
1482    ///
1483    /// Entries rejected by `valid_after` filtering are not raw storage misses and bypass
1484    /// admission. After an invalidation, admission therefore does not provide additional
1485    /// suppression for concurrent fills beyond the configured cache-lock behavior.
1486    ///
1487    /// # Panic
1488    /// Panic in other phases.
1489    pub async fn cache_lookup(&mut self) -> Result<Option<(CacheMeta, HitHandler)>> {
1490        match self.phase {
1491            // Stale is allowed here because stale-> cache_lock -> lookup again
1492            CachePhase::CacheKey | CachePhase::Stale => {
1493                let observe_admission =
1494                    self.phase == CachePhase::CacheKey && self.digest.admission.is_none();
1495                let (result, admission) = {
1496                    let inner = self
1497                        .inner
1498                        .as_mut()
1499                        .expect("Cache phase is checked and should have inner");
1500                    let inner_enabled = inner
1501                        .enabled_ctx
1502                        .as_mut()
1503                        .expect("Cache enabled on cache_lookup");
1504                    #[cfg_attr(not(feature = "trace"), allow(unused_mut))]
1505                    let mut span = inner_enabled.traces.child("lookup");
1506                    let key = inner.key.as_ref().unwrap(); // safe, this phase should have cache key
1507                    let now = Instant::now();
1508                    let result = inner_enabled.storage.lookup(key, &span.handle()).await?;
1509                    // one request may have multiple lookups
1510                    self.digest.add_lookup_duration(now.elapsed());
1511                    let storage_miss = result.is_none();
1512                    let result = result.and_then(|(meta, header)| {
1513                        if let Some(ts) = inner_enabled.valid_after {
1514                            // `created` (not `provenance`) is the right field to compare on
1515                            // the variant side: we are asking "was this specific variant
1516                            // admitted before the primary's tombstone?" -- a fact about the
1517                            // variant entry itself.
1518                            if meta.created() < ts {
1519                                span.set_tag(|| trace::Tag::new("not valid", true));
1520                                return None;
1521                            }
1522                        }
1523                        Some((meta, header))
1524                    });
1525                    let admission = (storage_miss && observe_admission)
1526                        .then(|| inner_enabled.admission.map(|policy| policy.observe(key)))
1527                        .flatten();
1528                    if let Some(decision) = admission {
1529                        span.set_tag(|| {
1530                            trace::Tag::new("admission.observed", decision.observed() as i64)
1531                        });
1532                        span.set_tag(|| {
1533                            trace::Tag::new("admission.deferred", decision.is_deferred())
1534                        });
1535                    }
1536                    if result.is_none() && admission.is_none_or(|decision| !decision.is_deferred())
1537                    {
1538                        if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1539                            lock_ctx.lock = Some(lock_ctx.cache_lock.lock(key, false));
1540                        }
1541                    }
1542                    span.set_tag(|| trace::Tag::new("found", result.is_some()));
1543                    (result, admission)
1544                };
1545                if let Some(decision) = admission {
1546                    self.digest.admission = Some(decision);
1547                    if decision.is_deferred() {
1548                        self.disable(NoCacheReason::Deferred);
1549                    }
1550                }
1551                Ok(result)
1552            }
1553            _ => panic!("wrong phase {:?}", self.phase),
1554        }
1555    }
1556
1557    /// Update variance and see if the meta matches the current variance
1558    ///
1559    /// `cache_lookup() -> compute vary hash -> cache_vary_lookup()`
1560    /// This function allows callers to compute vary based on the initial cache hit.
1561    /// `meta` should be the ones returned from the initial cache_lookup()
1562    /// - return true if the meta is the variance.
1563    /// - return false if the current meta doesn't match the variance, need to cache_lookup() again
1564    pub fn cache_vary_lookup(&mut self, variance: HashBinary, meta: &CacheMeta) -> bool {
1565        match self.phase {
1566            // Stale is allowed here because stale-> cache_lock -> lookup again
1567            CachePhase::CacheKey | CachePhase::Stale => {
1568                let inner = self.inner_mut();
1569                // make sure that all variances found are fresher than this asset
1570                // this is because when purging all the variance, only the primary slot is deleted
1571                // the provenance timestamp of the primary is the tombstone of all the variances
1572                inner
1573                    .enabled_ctx
1574                    .as_mut()
1575                    .expect("cache enabled")
1576                    .valid_after = Some(meta.provenance());
1577
1578                // update vary
1579                let key = inner.key.as_mut().unwrap();
1580                // if no variance was previously set, then this is the first cache hit
1581                let is_initial_cache_hit = key.get_variance_key().is_none();
1582                key.set_variance_key(variance);
1583                let variance_binary = key.variance_bin();
1584                let matches_variance = meta.variance() == variance_binary;
1585
1586                // We should remove the variance in the lookup `key` if this is the primary variant
1587                // slot. We know this is the primary variant slot if this is the initial cache hit,
1588                // AND the variance in the `key` already matches the `meta`'s.
1589                //
1590                // For the primary variant slot, the storage backend needs to use the primary key
1591                // for both cache lookup and updating the meta. Otherwise it will look for the
1592                // asset in the wrong location during revalidation.
1593                //
1594                // We can recreate the "full" cache key by using the meta's variance, if needed.
1595                if matches_variance && is_initial_cache_hit {
1596                    inner.key.as_mut().unwrap().remove_variance_key();
1597                }
1598
1599                matches_variance
1600            }
1601            _ => panic!("wrong phase {:?}", self.phase),
1602        }
1603    }
1604
1605    /// Whether this request is behind a cache lock in order to wait for another request to read the
1606    /// asset.
1607    pub fn is_cache_locked(&self) -> bool {
1608        matches!(
1609            self.inner_enabled()
1610                .lock_ctx
1611                .as_ref()
1612                .and_then(|l| l.lock.as_ref()),
1613            Some(Locked::Read(_))
1614        )
1615    }
1616
1617    /// Whether this request is the leader request to fetch the assets for itself and other requests
1618    /// behind the cache lock.
1619    pub fn is_cache_lock_writer(&self) -> bool {
1620        matches!(
1621            self.inner_enabled()
1622                .lock_ctx
1623                .as_ref()
1624                .and_then(|l| l.lock.as_ref()),
1625            Some(Locked::Write(_))
1626        )
1627    }
1628
1629    /// Maximum number of cache lock retries configured for this request.
1630    pub fn cache_lock_max_retries(&self) -> Option<usize> {
1631        self.inner_enabled()
1632            .lock_ctx
1633            .as_ref()
1634            .and_then(|l| l.max_retries)
1635    }
1636
1637    /// Take the write lock from this request to transfer it to another one.
1638    /// # Panic
1639    ///  Call is_cache_lock_writer() to check first, will panic otherwise.
1640    pub fn take_write_lock(&mut self) -> (WritePermit, &'static CacheKeyLockImpl) {
1641        let lock_ctx = self
1642            .inner_enabled_mut()
1643            .lock_ctx
1644            .as_mut()
1645            .expect("take_write_lock() called without cache lock");
1646        let lock = lock_ctx
1647            .lock
1648            .take()
1649            .expect("take_write_lock() called without lock");
1650        match lock {
1651            Locked::Write(w) => (w, lock_ctx.cache_lock),
1652            Locked::Read(_) => panic!("take_write_lock() called on read lock"),
1653        }
1654    }
1655
1656    /// Set the write lock, which is usually transferred from [Self::take_write_lock()]
1657    ///
1658    /// # Panic
1659    /// Panics if cache lock was not originally configured for this request.
1660    // TODO: it may make sense to allow configuring the CacheKeyLock here too that the write permit
1661    // is associated with
1662    // (The WritePermit comes from the CacheKeyLock and should be used when releasing from the CacheKeyLock,
1663    // shouldn't be possible to give a WritePermit to a request using a different CacheKeyLock)
1664    pub fn set_write_lock(&mut self, write_lock: WritePermit) {
1665        if let Some(lock_ctx) = self.inner_enabled_mut().lock_ctx.as_mut() {
1666            lock_ctx.lock.replace(Locked::Write(write_lock));
1667        }
1668    }
1669
1670    /// Whether this request's cache hit is staled
1671    fn has_staled_asset(&self) -> bool {
1672        matches!(self.phase, CachePhase::Stale | CachePhase::StaleUpdating)
1673    }
1674
1675    /// Whether this asset is staled and stale if error is allowed
1676    pub fn can_serve_stale_error(&self) -> bool {
1677        self.has_staled_asset() && self.cache_meta().serve_stale_if_error(SystemTime::now())
1678    }
1679
1680    /// Whether this asset is staled and stale while revalidate is allowed.
1681    pub fn can_serve_stale_updating(&self) -> bool {
1682        self.has_staled_asset()
1683            && self
1684                .cache_meta()
1685                .serve_stale_while_revalidate(SystemTime::now())
1686    }
1687
1688    /// Wait for the cache read lock to be unlocked
1689    ///
1690    /// A request carrying an [`lock::UnusableFills`] on its cache key can also stop
1691    /// early with [`LockWaitOutcome::Abandoned`], which [`Self::lock_abandon`] keeps
1692    /// afterwards.
1693    ///
1694    /// # Panic
1695    /// Check [Self::is_cache_locked()], panic if this request doesn't have a read lock.
1696    pub async fn cache_lock_wait(&mut self) -> LockWaitOutcome {
1697        // Taken before the mutable borrow below. Naming nothing waits as always.
1698        let unusable = self
1699            .maybe_cache_key()
1700            .and_then(|key| key.extensions.get::<UnusableFills>())
1701            .cloned();
1702
1703        let inner_enabled = self.inner_enabled_mut();
1704        #[cfg_attr(not(feature = "trace"), allow(unused_mut))]
1705        let mut span = inner_enabled.traces.child("cache_lock");
1706        // should always call is_cache_locked() before this function, which should guarantee that
1707        // the inner cache has a read lock and lock ctx
1708        let (read_lock, outcome) = if let Some(lock_ctx) = inner_enabled.lock_ctx.as_mut() {
1709            let lock = lock_ctx.lock.take(); // remove the lock from self
1710            if let Some(Locked::Read(r)) = lock {
1711                let now = Instant::now();
1712                // it's possible for a request to be locked more than once,
1713                // so wait the remainder of our configured timeout
1714                let wait = async {
1715                    match unusable.as_ref() {
1716                        Some(unusable) => r.wait_unless_published(unusable).await,
1717                        None => {
1718                            r.wait().await;
1719                            WaitOutcome::Released
1720                        }
1721                    }
1722                };
1723                let outcome = if let Some(wait_timeout) = lock_ctx.wait_timeout {
1724                    let wait_timeout =
1725                        wait_timeout.saturating_sub(self.lock_duration().unwrap_or(Duration::ZERO));
1726                    match timeout(wait_timeout, wait).await {
1727                        Ok(outcome) => Self::wait_result(&r, outcome),
1728                        Err(_) => LockWaitOutcome::WaitTimeout,
1729                    }
1730                } else {
1731                    Self::wait_result(&r, wait.await)
1732                };
1733                self.digest.add_lock_duration(now.elapsed());
1734                // On the digest as well as returned: a logging filter reports it
1735                // long after the caller has acted on the outcome.
1736                if let LockWaitOutcome::Abandoned { reason, token } = outcome {
1737                    self.digest.lock_abandon = Some(LockAbandon { reason, token });
1738                }
1739                (r, outcome)
1740            } else {
1741                panic!("cache_lock_wait on wrong type of lock")
1742            }
1743        } else {
1744            panic!("cache_lock_wait without cache lock")
1745        };
1746        if let Some(lock_ctx) = self.inner_enabled().lock_ctx.as_ref() {
1747            lock_ctx
1748                .cache_lock
1749                .trace_lock_wait(&mut span, &read_lock, outcome.lock_status());
1750        }
1751        outcome
1752    }
1753
1754    /// How long did this request wait behind the read lock
1755    pub fn lock_duration(&self) -> Option<Duration> {
1756        self.digest.lock_duration
1757    }
1758
1759    /// An abandoning reader's outcome is deliberately not written to the shared
1760    /// lock status: the writer and every other reader are unaffected.
1761    fn wait_result(lock: &lock::ReadLock, outcome: WaitOutcome) -> LockWaitOutcome {
1762        match outcome {
1763            WaitOutcome::Abandoned(matched) => LockWaitOutcome::Abandoned {
1764                reason: NoCacheReason::Custom(matched.reason),
1765                token: matched.token,
1766            },
1767            WaitOutcome::Released | WaitOutcome::AgeTimeout => {
1768                Self::released_result(lock.lock_status())
1769            }
1770        }
1771    }
1772
1773    /// A released lock should never still read [`LockStatus::Waiting`]. `Dangling`
1774    /// already means "bad state, recompete", and warns, so no panic is needed.
1775    fn released_result(status: LockStatus) -> LockWaitOutcome {
1776        match status {
1777            LockStatus::Done => LockWaitOutcome::Done,
1778            LockStatus::TransientError => LockWaitOutcome::TransientError,
1779            LockStatus::Dangling => LockWaitOutcome::Dangling,
1780            LockStatus::WaitTimeout => LockWaitOutcome::WaitTimeout,
1781            LockStatus::AgeTimeout => LockWaitOutcome::AgeTimeout,
1782            LockStatus::GiveUp => LockWaitOutcome::GiveUp,
1783            LockStatus::Waiting => {
1784                debug_assert!(false, "a released lock cannot still be Waiting");
1785                LockWaitOutcome::Dangling
1786            }
1787        }
1788    }
1789
1790    /// The fill this request stopped waiting over, and why it could not use it.
1791    ///
1792    /// Set only when [`Self::cache_lock_wait`] returned
1793    /// [`LockWaitOutcome::Abandoned`]. Absent for every other outcome, including
1794    /// [`LockWaitOutcome::GiveUp`], which is the writer giving up rather than this
1795    /// request abandoning the wait.
1796    pub fn lock_abandon(&self) -> Option<LockAbandon> {
1797        self.digest.lock_abandon
1798    }
1799
1800    /// How long did this request spent on cache lookup and reading the header
1801    pub fn lookup_duration(&self) -> Option<Duration> {
1802        self.digest.lookup_duration
1803    }
1804
1805    /// Return the [`Decision`] made for an absent cache key.
1806    pub fn admission_decision(&self) -> Option<Decision> {
1807        self.digest.admission
1808    }
1809
1810    /// Delete the asset from the cache storage
1811    /// # Panic
1812    /// Need to be called after the cache key is set. Panic otherwise.
1813    pub async fn purge(&self) -> Result<bool> {
1814        self.purge_action(PurgeAction::Delete).await
1815    }
1816
1817    /// Mark the asset stale in the cache storage so the next read revalidates it.
1818    ///
1819    /// Storage that cannot mark an asset stale deletes it instead, so this never leaves a
1820    /// fresh asset behind.
1821    ///
1822    /// # Panic
1823    /// Need to be called after the cache key is set. Panic otherwise.
1824    pub async fn expire(&self) -> Result<bool> {
1825        self.purge_action(PurgeAction::Expire).await
1826    }
1827
1828    async fn purge_action(&self, action: PurgeAction) -> Result<bool> {
1829        match self.phase {
1830            CachePhase::CacheKey => {
1831                let inner = self.inner();
1832                let inner_enabled = self.inner_enabled();
1833                let span = inner_enabled.traces.child("purge");
1834                let key = inner.key.as_ref().unwrap().to_compact();
1835                Self::purge_impl(
1836                    inner_enabled.storage,
1837                    inner_enabled.eviction,
1838                    &key,
1839                    action,
1840                    span,
1841                )
1842                .await
1843            }
1844            _ => panic!("wrong phase {:?}", self.phase),
1845        }
1846    }
1847
1848    /// Delete the asset from the cache storage via a spawned task.
1849    /// Returns corresponding `JoinHandle` of that task.
1850    /// # Panic
1851    /// Need to be called after the cache key is set. Panic otherwise.
1852    pub fn spawn_async_purge(
1853        &self,
1854        context: &'static str,
1855    ) -> tokio::task::JoinHandle<Result<bool>> {
1856        if matches!(self.phase, CachePhase::Disabled(_) | CachePhase::Uninit) {
1857            panic!("wrong phase {:?}", self.phase);
1858        }
1859
1860        let inner_enabled = self.inner_enabled();
1861        let span = inner_enabled.traces.child("purge");
1862        let key = self.inner().key.as_ref().unwrap().to_compact();
1863        let storage = inner_enabled.storage;
1864        let eviction = inner_enabled.eviction;
1865        tokio::task::spawn(async move {
1866            Self::purge_impl(storage, eviction, &key, PurgeAction::Delete, span)
1867                .await
1868                .map_err(|e| {
1869                    warn!("Failed to purge {key} (context: {context}): {e}");
1870                    e
1871                })
1872        })
1873    }
1874
1875    #[cfg_attr(not(feature = "trace"), allow(unused_mut))]
1876    async fn purge_impl(
1877        storage: &'static (dyn storage::Storage + Sync),
1878        eviction: Option<&'static (dyn eviction::EvictionManager + Sync)>,
1879        key: &CompactCacheKey,
1880        action: PurgeAction,
1881        mut span: Span,
1882    ) -> Result<bool> {
1883        let target = storage::PurgeTarget::Active(key);
1884        let result = match action {
1885            PurgeAction::Delete => {
1886                storage
1887                    .purge(target, PurgeType::Invalidation, &span.handle())
1888                    .await
1889            }
1890            PurgeAction::Expire => storage.expire(target, &span.handle()).await,
1891        };
1892        let purged = match result.as_ref() {
1893            Ok(storage::PurgeOutcome::NotFound) | Err(_) => false,
1894            Ok(storage::PurgeOutcome::Purged(entry_id)) => {
1895                if let Some(eviction) = eviction {
1896                    eviction.remove(target.removed_entry(*entry_id));
1897                }
1898                true
1899            }
1900            // the entry is still stored, so the eviction manager keeps tracking it
1901            Ok(storage::PurgeOutcome::Expired) => true,
1902        };
1903        span.set_tag(|| trace::Tag::new("purged", purged));
1904        // `purged` alone cannot tell an expiry apart from storage falling back to deleting, so
1905        // record what actually happened to the entry.
1906        span.set_tag(|| {
1907            trace::Tag::new(
1908                "purge_outcome",
1909                match result.as_ref() {
1910                    Ok(storage::PurgeOutcome::NotFound) => "not_found",
1911                    Ok(storage::PurgeOutcome::Purged(_)) => "deleted",
1912                    Ok(storage::PurgeOutcome::Expired) => "expired",
1913                    Err(_) => "error",
1914                },
1915            )
1916        });
1917        result?;
1918        Ok(purged)
1919    }
1920
1921    /// Check the cacheable prediction
1922    ///
1923    /// Return true if the predictor is not set
1924    pub fn cacheable_prediction(&self) -> bool {
1925        if let Some(predictor) = self.inner().predictor {
1926            predictor.cacheable_prediction(self.cache_key())
1927        } else {
1928            true
1929        }
1930    }
1931
1932    /// The reason the predictor remembered for this key when [Self::bypass] ran.
1933    ///
1934    /// `None` when the cache was not bypassed, when no predictor is configured, or when the
1935    /// predictor does not track reasons. Callers must treat `None` as "unknown" rather than
1936    /// as evidence about the previous response.
1937    pub fn predicted_uncacheable_reason(&self) -> Option<NoCacheReason> {
1938        self.inner
1939            .as_ref()
1940            .and_then(|inner| inner.predicted_uncacheable_reason)
1941    }
1942
1943    /// Tell the predictor that this response, which is previously predicted to be uncacheable,
1944    /// is cacheable now.
1945    pub fn response_became_cacheable(&self) {
1946        if let Some(predictor) = self.inner().predictor {
1947            predictor.mark_cacheable(self.cache_key());
1948        }
1949    }
1950
1951    /// Tell the predictor that this response is uncacheable so that it will know next time
1952    /// this request arrives.
1953    pub fn response_became_uncacheable(&self, reason: NoCacheReason) {
1954        if let Some(predictor) = self.inner().predictor {
1955            predictor.mark_uncacheable(self.cache_key(), reason);
1956        }
1957    }
1958
1959    /// Tag all spans as being part of a subrequest.
1960    pub fn tag_as_subrequest(&mut self) {
1961        self.inner_enabled_mut()
1962            .traces
1963            .cache_span
1964            .set_tag(|| Tag::new("is_subrequest", true))
1965    }
1966}
1967
1968#[cfg(test)]
1969mod tests {
1970    use super::*;
1971    use crate::lock::{CacheLock, UnusableFill};
1972    use async_trait::async_trait;
1973    use http::StatusCode;
1974    use std::any::Any;
1975    use std::num::NonZeroU32;
1976    use std::sync::atomic::{AtomicUsize, Ordering};
1977    use std::sync::{LazyLock, Mutex};
1978
1979    /// Storage fixture with successful metadata updates and configurable purge results.
1980    struct UpdateOkStorage {
1981        purge_ok: bool,
1982    }
1983    struct IdentifiedEntryStorage {
1984        append: bool,
1985    }
1986    struct OneShotLookupStorage {
1987        entries: Mutex<Vec<(CompactCacheKey, CacheMeta)>>,
1988    }
1989    /// Storage that can mark an entry stale, recording the target it was asked to expire. Its
1990    /// `purge` is unreachable so a test fails loudly if expiry is routed to deletion instead.
1991    struct ExpiringStorage {
1992        expired: Mutex<Option<CompactCacheKey>>,
1993    }
1994    struct EmptyHitHandler {
1995        entry_id: Option<u64>,
1996    }
1997    struct IdentifiedMissHandler {
1998        finish: MissFinishType,
1999    }
2000    struct CountingDeferPolicy(AtomicUsize);
2001    struct CountingReadyPolicy(AtomicUsize);
2002    #[derive(Default)]
2003    struct RecordingEviction {
2004        removed: Mutex<Option<eviction::CacheEntryKey>>,
2005        accessed: Mutex<Option<eviction::CacheEntryKey>>,
2006        admitted: Mutex<Option<eviction::CacheEntryKey>>,
2007        incremented: Mutex<Option<(eviction::CacheEntryKey, usize, Option<usize>)>>,
2008    }
2009
2010    static UPDATE_OK_STORAGE: UpdateOkStorage = UpdateOkStorage { purge_ok: false };
2011    static PURGE_OK_STORAGE: UpdateOkStorage = UpdateOkStorage { purge_ok: true };
2012    static IDENTIFIED_CREATED_STORAGE: IdentifiedEntryStorage =
2013        IdentifiedEntryStorage { append: false };
2014    static IDENTIFIED_APPENDED_STORAGE: IdentifiedEntryStorage =
2015        IdentifiedEntryStorage { append: true };
2016    // Only one test uses this storage. Keep it that way unless the tests also isolate their keys
2017    // and clear any entries they push.
2018    static ONE_SHOT_LOOKUP_STORAGE: OneShotLookupStorage = OneShotLookupStorage {
2019        entries: Mutex::new(Vec::new()),
2020    };
2021    static EXPIRING_STORAGE: ExpiringStorage = ExpiringStorage {
2022        expired: Mutex::new(None),
2023    };
2024    static RAW_MISS_DEFER_POLICY: CountingDeferPolicy = CountingDeferPolicy(AtomicUsize::new(0));
2025    static VALID_AFTER_DEFER_POLICY: CountingDeferPolicy = CountingDeferPolicy(AtomicUsize::new(0));
2026    static STALE_DEFER_POLICY: CountingDeferPolicy = CountingDeferPolicy(AtomicUsize::new(0));
2027    static RAW_MISS_READY_POLICY: CountingReadyPolicy = CountingReadyPolicy(AtomicUsize::new(0));
2028    static TWO_USE_ADMISSION_POLICY: LazyLock<admission::MinUsesAdmissionPolicy> =
2029        LazyLock::new(|| admission::MinUsesAdmissionPolicy::new(NonZeroU32::new(2).unwrap()));
2030    impl AdmissionPolicy for CountingDeferPolicy {
2031        fn observe(&self, _key: &CacheKey) -> Decision {
2032            self.0.fetch_add(1, Ordering::Relaxed);
2033            Decision::Defer { observed: 1 }
2034        }
2035    }
2036
2037    impl AdmissionPolicy for CountingReadyPolicy {
2038        fn observe(&self, _key: &CacheKey) -> Decision {
2039            let observed = self.0.fetch_add(1, Ordering::Relaxed) + 1;
2040            Decision::Ready {
2041                observed: observed as u32,
2042            }
2043        }
2044    }
2045
2046    #[async_trait]
2047    impl storage::HandleHit for EmptyHitHandler {
2048        async fn read_body(&mut self) -> Result<Option<bytes::Bytes>> {
2049            Ok(None)
2050        }
2051
2052        async fn finish(
2053            self: Box<Self>,
2054            _storage: &'static (dyn Storage + Sync),
2055            _key: &CacheKey,
2056            _trace: &trace::SpanHandle,
2057        ) -> Result<()> {
2058            Ok(())
2059        }
2060
2061        fn as_any(&self) -> &(dyn Any + Send + Sync) {
2062            self
2063        }
2064
2065        fn as_any_mut(&mut self) -> &mut (dyn Any + Send + Sync) {
2066            self
2067        }
2068
2069        fn entry_id(&self) -> Option<eviction::CacheEntryId> {
2070            self.entry_id.map(eviction::CacheEntryId::new)
2071        }
2072    }
2073
2074    #[async_trait]
2075    impl storage::HandleMiss for IdentifiedMissHandler {
2076        async fn write_body(&mut self, _data: bytes::Bytes, _eof: bool) -> Result<()> {
2077            Ok(())
2078        }
2079
2080        async fn finish(self: Box<Self>) -> Result<MissFinishType> {
2081            Ok(self.finish)
2082        }
2083
2084        fn entry_id(&self) -> Option<eviction::CacheEntryId> {
2085            Some(eviction::CacheEntryId::new(7))
2086        }
2087    }
2088
2089    #[async_trait]
2090    impl Storage for UpdateOkStorage {
2091        async fn lookup(
2092            &'static self,
2093            _key: &CacheKey,
2094            _trace: &trace::SpanHandle,
2095        ) -> Result<Option<(CacheMeta, HitHandler)>> {
2096            Ok(None)
2097        }
2098
2099        async fn get_miss_handler(
2100            &'static self,
2101            _key: &CacheKey,
2102            _meta: &CacheMeta,
2103            _trace: &trace::SpanHandle,
2104        ) -> Result<MissHandler> {
2105            unreachable!("tests do not write bodies through this storage")
2106        }
2107
2108        async fn purge(
2109            &'static self,
2110            _target: storage::PurgeTarget<'_>,
2111            _purge_type: PurgeType,
2112            _trace: &trace::SpanHandle,
2113        ) -> Result<storage::PurgeOutcome> {
2114            Ok(if self.purge_ok {
2115                storage::PurgeOutcome::Purged(None)
2116            } else {
2117                storage::PurgeOutcome::NotFound
2118            })
2119        }
2120
2121        async fn update_meta(
2122            &'static self,
2123            _key: &CacheKey,
2124            _meta: &CacheMeta,
2125            _trace: &trace::SpanHandle,
2126        ) -> Result<bool> {
2127            Ok(true)
2128        }
2129
2130        fn as_any(&self) -> &(dyn Any + Send + Sync + 'static) {
2131            self
2132        }
2133    }
2134
2135    #[async_trait]
2136    impl Storage for ExpiringStorage {
2137        async fn lookup(
2138            &'static self,
2139            _key: &CacheKey,
2140            _trace: &trace::SpanHandle,
2141        ) -> Result<Option<(CacheMeta, HitHandler)>> {
2142            Ok(None)
2143        }
2144
2145        async fn get_miss_handler(
2146            &'static self,
2147            _key: &CacheKey,
2148            _meta: &CacheMeta,
2149            _trace: &trace::SpanHandle,
2150        ) -> Result<MissHandler> {
2151            unreachable!("tests do not write bodies through this storage")
2152        }
2153
2154        async fn purge(
2155            &'static self,
2156            _target: storage::PurgeTarget<'_>,
2157            _purge_type: PurgeType,
2158            _trace: &trace::SpanHandle,
2159        ) -> Result<storage::PurgeOutcome> {
2160            unreachable!("storage that can expire must not be asked to delete")
2161        }
2162
2163        async fn expire(
2164            &'static self,
2165            target: storage::PurgeTarget<'_>,
2166            _trace: &trace::SpanHandle,
2167        ) -> Result<storage::PurgeOutcome> {
2168            *self.expired.lock().unwrap() = Some(target.key().clone());
2169            Ok(storage::PurgeOutcome::Expired)
2170        }
2171
2172        async fn update_meta(
2173            &'static self,
2174            _key: &CacheKey,
2175            _meta: &CacheMeta,
2176            _trace: &trace::SpanHandle,
2177        ) -> Result<bool> {
2178            Ok(true)
2179        }
2180
2181        fn as_any(&self) -> &(dyn Any + Send + Sync + 'static) {
2182            self
2183        }
2184    }
2185
2186    #[async_trait]
2187    impl Storage for IdentifiedEntryStorage {
2188        async fn lookup(
2189            &'static self,
2190            _key: &CacheKey,
2191            _trace: &trace::SpanHandle,
2192        ) -> Result<Option<(CacheMeta, HitHandler)>> {
2193            Ok(None)
2194        }
2195
2196        async fn get_miss_handler(
2197            &'static self,
2198            _key: &CacheKey,
2199            _meta: &CacheMeta,
2200            _trace: &trace::SpanHandle,
2201        ) -> Result<MissHandler> {
2202            let finish = if self.append {
2203                MissFinishType::Appended(2, Some(9))
2204            } else {
2205                MissFinishType::Created(1)
2206            };
2207            Ok(Box::new(IdentifiedMissHandler { finish }))
2208        }
2209
2210        async fn purge(
2211            &'static self,
2212            target: storage::PurgeTarget<'_>,
2213            _purge_type: PurgeType,
2214            _trace: &trace::SpanHandle,
2215        ) -> Result<storage::PurgeOutcome> {
2216            let entry_id = match target {
2217                storage::PurgeTarget::Active(_) => Some(eviction::CacheEntryId::new(1)),
2218                storage::PurgeTarget::Exact(_) => None,
2219            };
2220            Ok(storage::PurgeOutcome::Purged(entry_id))
2221        }
2222
2223        async fn update_meta(
2224            &'static self,
2225            _key: &CacheKey,
2226            _meta: &CacheMeta,
2227            _trace: &trace::SpanHandle,
2228        ) -> Result<bool> {
2229            Ok(true)
2230        }
2231
2232        fn as_any(&self) -> &(dyn Any + Send + Sync + 'static) {
2233            self
2234        }
2235    }
2236
2237    #[async_trait]
2238    impl eviction::EvictionManager for RecordingEviction {
2239        fn total_size(&self) -> usize {
2240            0
2241        }
2242
2243        fn total_items(&self) -> usize {
2244            0
2245        }
2246
2247        fn evicted_size(&self) -> usize {
2248            0
2249        }
2250
2251        fn evicted_items(&self) -> usize {
2252            0
2253        }
2254
2255        fn admit(
2256            &self,
2257            item: eviction::CacheEntryKey,
2258            _size: usize,
2259            _fresh_until: SystemTime,
2260        ) -> Vec<eviction::CacheEntryKey> {
2261            *self.admitted.lock().unwrap() = Some(item);
2262            Vec::new()
2263        }
2264
2265        fn increment_weight(
2266            &self,
2267            item: &eviction::CacheEntryKey,
2268            delta: usize,
2269            max_weight: Option<usize>,
2270        ) -> Vec<eviction::CacheEntryKey> {
2271            *self.incremented.lock().unwrap() = Some((item.clone(), delta, max_weight));
2272            Vec::new()
2273        }
2274
2275        fn remove(&self, item: eviction::CacheEntryKeyRef<'_>) {
2276            *self.removed.lock().unwrap() = Some(eviction::CacheEntryKey::from_entry_id(
2277                item.key().clone(),
2278                item.entry_id(),
2279            ));
2280        }
2281
2282        fn access(
2283            &self,
2284            item: &eviction::CacheEntryKey,
2285            _size: usize,
2286            _fresh_until: SystemTime,
2287        ) -> bool {
2288            *self.accessed.lock().unwrap() = Some(item.clone());
2289            true
2290        }
2291
2292        fn peek(&self, _item: &eviction::CacheEntryKey) -> bool {
2293            false
2294        }
2295
2296        async fn save(&self, _dir_path: &str) -> Result<()> {
2297            Ok(())
2298        }
2299
2300        async fn load(&self, _dir_path: &str) -> Result<()> {
2301            Ok(())
2302        }
2303    }
2304
2305    #[async_trait]
2306    impl Storage for OneShotLookupStorage {
2307        async fn lookup(
2308            &'static self,
2309            key: &CacheKey,
2310            _trace: &trace::SpanHandle,
2311        ) -> Result<Option<(CacheMeta, HitHandler)>> {
2312            let compact_key = key.to_compact();
2313            let mut entries = self.entries.lock().unwrap();
2314            let Some(pos) = entries
2315                .iter()
2316                .position(|(entry_key, _)| entry_key == &compact_key)
2317            else {
2318                return Ok(None);
2319            };
2320            let (_, meta) = entries.remove(pos);
2321            Ok(Some((meta, Box::new(EmptyHitHandler { entry_id: None }))))
2322        }
2323
2324        async fn get_miss_handler(
2325            &'static self,
2326            _key: &CacheKey,
2327            _meta: &CacheMeta,
2328            _trace: &trace::SpanHandle,
2329        ) -> Result<MissHandler> {
2330            unreachable!("tests do not write bodies through this storage")
2331        }
2332
2333        async fn purge(
2334            &'static self,
2335            _target: storage::PurgeTarget<'_>,
2336            _purge_type: PurgeType,
2337            _trace: &trace::SpanHandle,
2338        ) -> Result<storage::PurgeOutcome> {
2339            Ok(storage::PurgeOutcome::NotFound)
2340        }
2341
2342        async fn update_meta(
2343            &'static self,
2344            _key: &CacheKey,
2345            _meta: &CacheMeta,
2346            _trace: &trace::SpanHandle,
2347        ) -> Result<bool> {
2348            Ok(true)
2349        }
2350
2351        fn as_any(&self) -> &(dyn Any + Send + Sync + 'static) {
2352            self
2353        }
2354    }
2355
2356    fn test_meta(created: SystemTime) -> CacheMeta {
2357        let header = ResponseHeader::build(StatusCode::OK, None).unwrap();
2358        CacheMeta::new(created + Duration::from_secs(60), created, 30, 30, header)
2359    }
2360
2361    fn cache_with_stale_meta(meta: CacheMeta, key: CacheKey) -> HttpCache {
2362        let mut cache = HttpCache::new();
2363        cache.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2364        cache.set_cache_key(key);
2365        cache.phase = CachePhase::Stale;
2366        cache.inner_enabled_mut().meta = Some(meta);
2367        cache
2368    }
2369
2370    static FILL_INTEREST_LOCK: LazyLock<CacheLock> =
2371        LazyLock::new(|| CacheLock::new(Duration::from_secs(30)));
2372
2373    const WRONG_PLACE: u64 = 7;
2374    const SOMEWHERE_ELSE: u64 = 9;
2375
2376    /// Reasons are the application's, not the cache's; it wraps them as
2377    /// [`NoCacheReason::Custom`].
2378    const NO_GOOD: &str = "NoGoodToThisReader";
2379    const NO_GOOD_EITHER: &str = "AlsoNoGood";
2380
2381    fn cannot_use(token: u64) -> UnusableFills {
2382        UnusableFills {
2383            fills: vec![UnusableFill {
2384                token,
2385                reason: NO_GOOD,
2386            }]
2387            .into(),
2388        }
2389    }
2390
2391    fn locked_reader(key: &str, interest: Option<UnusableFills>) -> HttpCache {
2392        let mut cache_key = CacheKey::new(key, "");
2393        if let Some(interest) = interest {
2394            cache_key.extensions.insert(interest);
2395        }
2396        let mut cache = HttpCache::new();
2397        cache.enable(
2398            &UPDATE_OK_STORAGE,
2399            None,
2400            None,
2401            Some(&*FILL_INTEREST_LOCK),
2402            None,
2403        );
2404        cache.set_cache_key(cache_key);
2405        cache
2406    }
2407
2408    /// A reader that stops waiting reports `GiveUp` with its own reason, so the
2409    /// give-up is attributed to why it stopped rather than the generic
2410    /// `CacheLockGiveUp`.
2411    #[tokio::test]
2412    async fn a_reader_that_stops_waiting_reports_its_own_reason() {
2413        let key = "stops-waiting";
2414
2415        let mut writer = locked_reader(key, None);
2416        assert!(writer.cache_lookup().await.unwrap().is_none());
2417        assert!(!writer.is_cache_locked(), "the first request is the writer");
2418
2419        let mut reader = locked_reader(key, Some(cannot_use(WRONG_PLACE)));
2420        assert!(reader.cache_lookup().await.unwrap().is_none());
2421        assert!(reader.is_cache_locked(), "the second request coalesces");
2422
2423        // The writer learns where it is filling from, and says so.
2424        let waiting = tokio::spawn(async move {
2425            let status = reader.cache_lock_wait().await;
2426            (status, reader.lock_abandon())
2427        });
2428        tokio::task::yield_now().await;
2429        assert!(!waiting.is_finished(), "nothing published yet");
2430
2431        writer.lock_publish_fill_tokens(&[WRONG_PLACE]);
2432
2433        assert_eq!(
2434            waiting.await.unwrap(),
2435            (
2436                LockWaitOutcome::Abandoned {
2437                    reason: NoCacheReason::Custom(NO_GOOD),
2438                    token: WRONG_PLACE,
2439                },
2440                Some(LockAbandon {
2441                    reason: NoCacheReason::Custom(NO_GOOD),
2442                    token: WRONG_PLACE,
2443                })
2444            ),
2445            "its own reason and token, on the outcome and on the digest alike"
2446        );
2447
2448        // The writer never released, so a reader arriving afterwards without an
2449        // interest still coalesces behind it.
2450        let mut other = locked_reader(key, None);
2451        assert!(other.cache_lookup().await.unwrap().is_none());
2452        assert!(other.is_cache_locked(), "the lock is untouched");
2453
2454        writer.release_write_lock(NoCacheReason::StorageError);
2455    }
2456
2457    /// The reason follows the token that matched, not the set: a key carries one
2458    /// [`UnusableFills`], so unrelated parts of an application share it.
2459    #[tokio::test]
2460    async fn the_reason_comes_from_the_token_that_matched() {
2461        let key = "reason-per-token";
2462
2463        let mut writer = locked_reader(key, None);
2464        assert!(writer.cache_lookup().await.unwrap().is_none());
2465
2466        let interest = UnusableFills {
2467            fills: vec![
2468                UnusableFill {
2469                    token: WRONG_PLACE,
2470                    reason: NO_GOOD,
2471                },
2472                UnusableFill {
2473                    token: SOMEWHERE_ELSE,
2474                    reason: NO_GOOD_EITHER,
2475                },
2476            ]
2477            .into(),
2478        };
2479        let mut reader = locked_reader(key, Some(interest));
2480        assert!(reader.cache_lookup().await.unwrap().is_none());
2481        assert!(reader.is_cache_locked());
2482
2483        let waiting = tokio::spawn(async move {
2484            let status = reader.cache_lock_wait().await;
2485            (status, reader.lock_abandon())
2486        });
2487        tokio::task::yield_now().await;
2488
2489        // Only the second token is published, so only its reason may surface.
2490        writer.lock_publish_fill_tokens(&[SOMEWHERE_ELSE]);
2491
2492        assert_eq!(
2493            waiting.await.unwrap(),
2494            (
2495                LockWaitOutcome::Abandoned {
2496                    reason: NoCacheReason::Custom(NO_GOOD_EITHER),
2497                    token: SOMEWHERE_ELSE,
2498                },
2499                Some(LockAbandon {
2500                    reason: NoCacheReason::Custom(NO_GOOD_EITHER),
2501                    token: SOMEWHERE_ELSE,
2502                })
2503            ),
2504            "the matched token's own reason, not the first in the set"
2505        );
2506
2507        writer.release_write_lock(NoCacheReason::StorageError);
2508    }
2509
2510    /// A reader whose tokens are never published waits for the writer, as before.
2511    #[tokio::test]
2512    async fn a_reader_naming_unpublished_tokens_still_waits() {
2513        let key = "unpublished-tokens";
2514
2515        let mut writer = locked_reader(key, None);
2516        assert!(writer.cache_lookup().await.unwrap().is_none());
2517
2518        let mut reader = locked_reader(key, Some(cannot_use(WRONG_PLACE)));
2519        assert!(reader.cache_lookup().await.unwrap().is_none());
2520        assert!(reader.is_cache_locked());
2521
2522        let waiting = tokio::spawn(async move { reader.cache_lock_wait().await });
2523        tokio::task::yield_now().await;
2524        assert!(!waiting.is_finished(), "the reader is still coalescing");
2525
2526        writer.release_write_lock(NoCacheReason::StorageError);
2527
2528        assert_eq!(waiting.await.unwrap(), LockWaitOutcome::TransientError);
2529    }
2530
2531    fn cache_with_lookup_storage(key: CacheKey) -> HttpCache {
2532        let mut cache = HttpCache::new();
2533        cache.enable(&ONE_SHOT_LOOKUP_STORAGE, None, None, None, None);
2534        cache.set_cache_key(key);
2535        cache
2536    }
2537
2538    #[tokio::test]
2539    async fn purge_removes_identified_entry() {
2540        let recording = Box::leak(Box::new(RecordingEviction::default()));
2541        let key = CacheKey::new("expanded-purge", "").to_compact();
2542
2543        assert!(HttpCache::purge_impl(
2544            &IDENTIFIED_CREATED_STORAGE,
2545            Some(recording),
2546            &key,
2547            PurgeAction::Delete,
2548            trace::Span::inactive(),
2549        )
2550        .await
2551        .unwrap());
2552
2553        let removed = recording
2554            .removed
2555            .lock()
2556            .unwrap()
2557            .take()
2558            .expect("purge should remove the identified entry");
2559        assert_eq!(
2560            removed,
2561            eviction::CacheEntryKey::identified(key, eviction::CacheEntryId::new(1))
2562        );
2563        assert!(recording.accessed.lock().unwrap().is_none());
2564        assert!(recording.admitted.lock().unwrap().is_none());
2565        assert!(recording.incremented.lock().unwrap().is_none());
2566    }
2567
2568    #[tokio::test]
2569    async fn purge_removes_key_only_entry() {
2570        let recording = Box::leak(Box::new(RecordingEviction::default()));
2571        let key = CacheKey::new("key-only-purge", "").to_compact();
2572
2573        assert!(HttpCache::purge_impl(
2574            &PURGE_OK_STORAGE,
2575            Some(recording),
2576            &key,
2577            PurgeAction::Delete,
2578            trace::Span::inactive(),
2579        )
2580        .await
2581        .unwrap());
2582
2583        let removed = recording
2584            .removed
2585            .lock()
2586            .unwrap()
2587            .take()
2588            .expect("purge should remove the key-only entry");
2589        assert_eq!(removed, eviction::CacheEntryKey::key_only(key));
2590    }
2591
2592    #[tokio::test]
2593    async fn expiring_an_entry_keeps_it_tracked_by_eviction() {
2594        let recording = Box::leak(Box::new(RecordingEviction::default()));
2595        let key = CacheKey::new("expire-keeps-entry", "").to_compact();
2596
2597        assert!(HttpCache::purge_impl(
2598            &EXPIRING_STORAGE,
2599            Some(recording),
2600            &key,
2601            PurgeAction::Expire,
2602            trace::Span::inactive(),
2603        )
2604        .await
2605        .unwrap());
2606
2607        assert_eq!(
2608            EXPIRING_STORAGE.expired.lock().unwrap().take(),
2609            Some(key),
2610            "storage should have been asked to expire the target"
2611        );
2612        assert!(
2613            recording.removed.lock().unwrap().is_none(),
2614            "the entry is still stored, so eviction must keep tracking it"
2615        );
2616    }
2617
2618    #[tokio::test]
2619    async fn expiring_falls_back_to_deleting_when_storage_cannot_mark_stale() {
2620        let recording = Box::leak(Box::new(RecordingEviction::default()));
2621        let key = CacheKey::new("expire-falls-back", "").to_compact();
2622
2623        assert!(HttpCache::purge_impl(
2624            &PURGE_OK_STORAGE,
2625            Some(recording),
2626            &key,
2627            PurgeAction::Expire,
2628            trace::Span::inactive(),
2629        )
2630        .await
2631        .unwrap());
2632
2633        let removed = recording
2634            .removed
2635            .lock()
2636            .unwrap()
2637            .take()
2638            .expect("the fallback deletes, so eviction must stop tracking the entry");
2639        assert_eq!(removed, eviction::CacheEntryKey::key_only(key));
2640    }
2641
2642    #[tokio::test]
2643    async fn the_expire_fallback_reports_nothing_purged_for_an_absent_entry() {
2644        let recording = Box::leak(Box::new(RecordingEviction::default()));
2645        let key = CacheKey::new("expire-absent", "").to_compact();
2646
2647        assert!(!HttpCache::purge_impl(
2648            &UPDATE_OK_STORAGE,
2649            Some(recording),
2650            &key,
2651            PurgeAction::Expire,
2652            trace::Span::inactive(),
2653        )
2654        .await
2655        .unwrap());
2656
2657        assert!(recording.removed.lock().unwrap().is_none());
2658    }
2659
2660    #[test]
2661    fn cache_hit_passes_entry_id_to_eviction() {
2662        let recording = Box::leak(Box::new(RecordingEviction::default()));
2663        let key = CacheKey::new("identified-hit", "");
2664        let mut cache = HttpCache::new();
2665        cache.enable(
2666            &IDENTIFIED_CREATED_STORAGE,
2667            Some(recording),
2668            None,
2669            None,
2670            None,
2671        );
2672        cache.set_cache_key(key.clone());
2673        cache.cache_found(
2674            test_meta(SystemTime::now()),
2675            Box::new(EmptyHitHandler { entry_id: Some(7) }),
2676            HitStatus::Fresh,
2677        );
2678
2679        assert_eq!(
2680            recording.accessed.lock().unwrap().take(),
2681            Some(eviction::CacheEntryKey::identified(
2682                key.to_compact(),
2683                eviction::CacheEntryId::new(7)
2684            ))
2685        );
2686        assert!(recording.removed.lock().unwrap().is_none());
2687        assert!(recording.admitted.lock().unwrap().is_none());
2688        assert!(recording.incremented.lock().unwrap().is_none());
2689    }
2690
2691    #[test]
2692    fn a_forced_expiry_that_serves_stale_is_stale_with_its_windows_intact() {
2693        let mut cache = HttpCache::new();
2694        cache.enable(&IDENTIFIED_CREATED_STORAGE, None, None, None, None);
2695        cache.set_cache_key(CacheKey::new("force-expired-serve-stale", ""));
2696        cache.cache_found(
2697            test_meta(SystemTime::now()),
2698            Box::new(EmptyHitHandler { entry_id: None }),
2699            HitStatus::ForceExpiredServeStale,
2700        );
2701
2702        assert_eq!(cache.phase(), CachePhase::Stale);
2703        assert_eq!(cache.cache_meta().stale_while_revalidate_sec(), 30);
2704        assert_eq!(cache.cache_meta().stale_if_error_sec(), 30);
2705    }
2706
2707    #[test]
2708    fn a_forced_expiry_that_serves_stale_is_neither_fresh_nor_a_miss() {
2709        let status = HitStatus::ForceExpiredServeStale;
2710
2711        assert!(!status.is_fresh());
2712        assert!(!status.is_treated_as_miss());
2713        assert_eq!(status.as_str(), "force_expired_serve_stale");
2714    }
2715
2716    #[tokio::test]
2717    async fn cache_miss_passes_entry_id_to_eviction() {
2718        let recording = Box::leak(Box::new(RecordingEviction::default()));
2719        let key = CacheKey::new("identified-miss", "");
2720        let mut cache = HttpCache::new();
2721        cache.enable(
2722            &IDENTIFIED_CREATED_STORAGE,
2723            Some(recording),
2724            None,
2725            None,
2726            None,
2727        );
2728        cache.set_cache_key(key.clone());
2729        cache.cache_miss();
2730        cache.set_cache_meta(test_meta(SystemTime::now()));
2731        cache.set_miss_handler().await.unwrap();
2732        cache.finish_miss_handler().await.unwrap();
2733        assert_eq!(
2734            recording.admitted.lock().unwrap().take(),
2735            Some(eviction::CacheEntryKey::identified(
2736                key.to_compact(),
2737                eviction::CacheEntryId::new(7)
2738            ))
2739        );
2740        assert!(recording.incremented.lock().unwrap().is_none());
2741        assert!(recording.removed.lock().unwrap().is_none());
2742        assert!(recording.accessed.lock().unwrap().is_none());
2743
2744        let mut cache = HttpCache::new();
2745        cache.enable(
2746            &IDENTIFIED_APPENDED_STORAGE,
2747            Some(recording),
2748            None,
2749            None,
2750            None,
2751        );
2752        cache.set_cache_key(key.clone());
2753        cache.cache_miss();
2754        cache.set_cache_meta(test_meta(SystemTime::now()));
2755        cache.set_miss_handler().await.unwrap();
2756        cache.finish_miss_handler().await.unwrap();
2757        assert_eq!(
2758            recording.incremented.lock().unwrap().take(),
2759            Some((
2760                eviction::CacheEntryKey::identified(
2761                    key.to_compact(),
2762                    eviction::CacheEntryId::new(7)
2763                ),
2764                2,
2765                Some(9)
2766            ))
2767        );
2768        assert!(recording.admitted.lock().unwrap().is_none());
2769        assert!(recording.removed.lock().unwrap().is_none());
2770        assert!(recording.accessed.lock().unwrap().is_none());
2771    }
2772
2773    #[tokio::test]
2774    async fn raw_storage_miss_can_defer_admission() {
2775        RAW_MISS_DEFER_POLICY.0.store(0, Ordering::Relaxed);
2776        let mut cache = HttpCache::new();
2777        cache.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2778        cache.set_admission_policy(&RAW_MISS_DEFER_POLICY);
2779        cache.set_cache_key(CacheKey::new("deferred-storage-miss", ""));
2780
2781        assert!(cache.cache_lookup().await.unwrap().is_none());
2782        assert_eq!(cache.phase(), CachePhase::Disabled(NoCacheReason::Deferred));
2783        assert_eq!(
2784            cache.admission_decision(),
2785            Some(Decision::Defer { observed: 1 })
2786        );
2787        assert_eq!(RAW_MISS_DEFER_POLICY.0.load(Ordering::Relaxed), 1);
2788    }
2789
2790    #[tokio::test]
2791    async fn second_storage_miss_can_proceed_to_fill() {
2792        let key = CacheKey::new("two-use-admission", "");
2793
2794        let mut first = HttpCache::new();
2795        first.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2796        first.set_admission_policy(&*TWO_USE_ADMISSION_POLICY);
2797        first.set_cache_key(key.clone());
2798        assert!(first.cache_lookup().await.unwrap().is_none());
2799        assert_eq!(first.phase(), CachePhase::Disabled(NoCacheReason::Deferred));
2800
2801        let mut second = HttpCache::new();
2802        second.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2803        second.set_admission_policy(&*TWO_USE_ADMISSION_POLICY);
2804        second.set_cache_key(key);
2805        assert!(second.cache_lookup().await.unwrap().is_none());
2806        assert_eq!(
2807            second.admission_decision(),
2808            Some(Decision::Ready { observed: 2 })
2809        );
2810        assert_eq!(second.phase(), CachePhase::CacheKey);
2811        second.cache_miss();
2812        assert_eq!(second.phase(), CachePhase::Miss);
2813    }
2814
2815    #[tokio::test]
2816    async fn repeated_raw_miss_is_observed_once_per_request() {
2817        RAW_MISS_READY_POLICY.0.store(0, Ordering::Relaxed);
2818        let mut cache = HttpCache::new();
2819        cache.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2820        cache.set_admission_policy(&RAW_MISS_READY_POLICY);
2821        cache.set_cache_key(CacheKey::new("repeated-ready-admission", ""));
2822
2823        assert!(cache.cache_lookup().await.unwrap().is_none());
2824        assert!(cache.cache_lookup().await.unwrap().is_none());
2825        assert_eq!(RAW_MISS_READY_POLICY.0.load(Ordering::Relaxed), 1);
2826        assert_eq!(
2827            cache.admission_decision(),
2828            Some(Decision::Ready { observed: 1 })
2829        );
2830    }
2831
2832    #[tokio::test]
2833    async fn stale_refill_does_not_observe_admission() {
2834        STALE_DEFER_POLICY.0.store(0, Ordering::Relaxed);
2835        let key = CacheKey::new("stale-refill-admission", "");
2836        let mut cache = HttpCache::new();
2837        cache.enable(&UPDATE_OK_STORAGE, None, None, None, None);
2838        cache.set_admission_policy(&STALE_DEFER_POLICY);
2839        cache.set_cache_key(key);
2840        cache.phase = CachePhase::Stale;
2841        cache.inner_enabled_mut().meta = Some(test_meta(SystemTime::now()));
2842
2843        assert!(cache.cache_lookup().await.unwrap().is_none());
2844        assert_eq!(STALE_DEFER_POLICY.0.load(Ordering::Relaxed), 0);
2845        assert_eq!(cache.admission_decision(), None);
2846        cache.cache_miss();
2847        assert_eq!(cache.phase(), CachePhase::Miss);
2848    }
2849
2850    #[tokio::test]
2851    async fn valid_after_rejection_does_not_observe_admission() {
2852        VALID_AFTER_DEFER_POLICY.0.store(0, Ordering::Relaxed);
2853        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2854        let key = CacheKey::new("valid-after-not-admission", "");
2855        ONE_SHOT_LOOKUP_STORAGE
2856            .entries
2857            .lock()
2858            .unwrap()
2859            .push((key.to_compact(), test_meta(created)));
2860
2861        let mut cache = HttpCache::new();
2862        cache.enable(&ONE_SHOT_LOOKUP_STORAGE, None, None, None, None);
2863        cache.set_admission_policy(&VALID_AFTER_DEFER_POLICY);
2864        cache.set_cache_key(key);
2865        cache.inner_enabled_mut().valid_after = Some(created + Duration::from_secs(1));
2866
2867        assert!(cache.cache_lookup().await.unwrap().is_none());
2868        assert_eq!(cache.phase(), CachePhase::CacheKey);
2869        assert_eq!(cache.admission_decision(), None);
2870        assert_eq!(VALID_AFTER_DEFER_POLICY.0.load(Ordering::Relaxed), 0);
2871    }
2872
2873    #[test]
2874    fn test_set_cache_meta_preserves_stale_provenance() {
2875        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2876        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
2877        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2878        let variance = [1; 16];
2879
2880        let mut old_meta = test_meta(created);
2881        old_meta.set_provenance(family_start);
2882        old_meta.set_variance_key(variance);
2883        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("preserve", ""));
2884
2885        cache.set_cache_meta(test_meta(refresh));
2886
2887        assert_eq!(cache.phase(), CachePhase::Expired);
2888        assert_eq!(cache.cache_meta().created(), refresh);
2889        assert_eq!(cache.cache_meta().provenance(), family_start);
2890        assert_eq!(cache.inner_enabled().stale_meta_variance, Some(variance));
2891    }
2892
2893    #[tokio::test]
2894    async fn test_revalidate_cache_meta_preserves_created_and_provenance() {
2895        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2896        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
2897        let revalidated_at = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2898
2899        let mut old_meta = test_meta(created);
2900        old_meta.set_provenance(family_start);
2901        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("revalidate", ""));
2902
2903        cache
2904            .revalidate_cache_meta(test_meta(revalidated_at))
2905            .await
2906            .unwrap();
2907
2908        assert_eq!(cache.phase(), CachePhase::Revalidated);
2909        assert_eq!(cache.cache_meta().created(), created);
2910        assert_eq!(cache.cache_meta().updated(), revalidated_at);
2911        assert_eq!(cache.cache_meta().provenance(), family_start);
2912    }
2913
2914    #[tokio::test]
2915    async fn revalidating_an_expired_meta_makes_it_fresh_again() {
2916        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2917        let expired_at = created + Duration::from_secs(30);
2918        let revalidated_at = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2919
2920        // An expiry has to be undone by revalidation, or a soft purge would leave the asset
2921        // stale forever. It comes back because revalidation replaces the whole meta rather than
2922        // merging into the expired one.
2923        let mut old_meta = test_meta(created);
2924        old_meta.expire_at(expired_at);
2925        assert!(!old_meta.is_fresh(expired_at + Duration::from_secs(1)));
2926
2927        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("revalidate-expired", ""));
2928        cache
2929            .revalidate_cache_meta(test_meta(revalidated_at))
2930            .await
2931            .unwrap();
2932
2933        let meta = cache.cache_meta();
2934        assert!(meta.is_fresh(revalidated_at));
2935        assert_eq!(meta.fresh_until(), revalidated_at + Duration::from_secs(60));
2936    }
2937
2938    #[test]
2939    fn test_update_variance_preserves_provenance_when_primary_variance_unchanged() {
2940        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2941        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
2942        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2943        let variance = [1; 16];
2944
2945        let mut old_meta = test_meta(created);
2946        old_meta.set_provenance(family_start);
2947        old_meta.set_variance_key(variance);
2948        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("same-vary", ""));
2949
2950        cache.set_cache_meta(test_meta(refresh));
2951        cache.update_variance(Some(variance));
2952
2953        assert_eq!(cache.cache_meta().provenance(), family_start);
2954        assert_eq!(cache.cache_meta().variance(), Some(variance));
2955        assert!(cache.cache_key().get_variance_key().is_none());
2956    }
2957
2958    #[test]
2959    fn test_update_variance_resets_provenance_when_primary_variance_changes() {
2960        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2961        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
2962        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2963        let old_variance = [1; 16];
2964        let new_variance = [2; 16];
2965
2966        let mut old_meta = test_meta(created);
2967        old_meta.set_provenance(family_start);
2968        old_meta.set_variance_key(old_variance);
2969        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("changed-vary", ""));
2970
2971        cache.set_cache_meta(test_meta(refresh));
2972        cache.update_variance(Some(new_variance));
2973
2974        assert_eq!(cache.cache_meta().provenance(), refresh);
2975        assert_eq!(cache.cache_meta().variance(), Some(new_variance));
2976        assert!(cache.cache_key().get_variance_key().is_none());
2977    }
2978
2979    #[test]
2980    fn test_update_variance_resets_provenance_when_primary_variance_appears() {
2981        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
2982        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
2983        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
2984        let variance = [1; 16];
2985
2986        let mut old_meta = test_meta(created);
2987        old_meta.set_provenance(family_start);
2988        let mut cache = cache_with_stale_meta(old_meta, CacheKey::new("vary-appears", ""));
2989
2990        cache.set_cache_meta(test_meta(refresh));
2991        cache.update_variance(Some(variance));
2992
2993        assert_eq!(cache.cache_meta().provenance(), refresh);
2994        assert_eq!(cache.cache_meta().variance(), Some(variance));
2995        assert!(cache.cache_key().get_variance_key().is_none());
2996    }
2997
2998    #[test]
2999    fn test_update_variance_resets_provenance_when_secondary_takes_primary_slot() {
3000        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
3001        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
3002        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
3003        let old_variance = [1; 16];
3004        let mut key = CacheKey::new("secondary-takeover", "");
3005        key.set_variance_key(old_variance);
3006
3007        let mut old_meta = test_meta(created);
3008        old_meta.set_provenance(family_start);
3009        old_meta.set_variance_key(old_variance);
3010        let mut cache = cache_with_stale_meta(old_meta, key);
3011
3012        cache.set_cache_meta(test_meta(refresh));
3013        cache.update_variance(None);
3014
3015        assert_eq!(cache.cache_meta().provenance(), refresh);
3016        assert!(cache.cache_meta().variance().is_none());
3017        assert!(cache.cache_key().get_variance_key().is_none());
3018    }
3019
3020    #[test]
3021    fn test_update_variance_preserves_provenance_when_secondary_variance_unchanged() {
3022        let created = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
3023        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(80);
3024        let refresh = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
3025        let variance = [1; 16];
3026        let mut key = CacheKey::new("secondary-same-vary", "");
3027        key.set_variance_key(variance);
3028
3029        let mut old_meta = test_meta(created);
3030        old_meta.set_provenance(family_start);
3031        old_meta.set_variance_key(variance);
3032        let mut cache = cache_with_stale_meta(old_meta, key);
3033
3034        cache.set_cache_meta(test_meta(refresh));
3035        cache.update_variance(Some(variance));
3036
3037        assert_eq!(cache.cache_meta().provenance(), family_start);
3038        assert_eq!(cache.cache_meta().variance(), Some(variance));
3039        assert_eq!(cache.cache_key().get_variance_key(), Some(&variance));
3040    }
3041
3042    #[tokio::test]
3043    async fn test_cache_vary_lookup_uses_provenance_for_valid_after() {
3044        let family_start = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
3045        let secondary_created = SystemTime::UNIX_EPOCH + Duration::from_secs(150);
3046        let primary_refreshed = SystemTime::UNIX_EPOCH + Duration::from_secs(200);
3047        let primary_variance = [1; 16];
3048        let secondary_variance = [2; 16];
3049
3050        let mut primary_meta = test_meta(primary_refreshed);
3051        primary_meta.set_provenance(family_start);
3052        primary_meta.set_variance_key(primary_variance);
3053
3054        let mut secondary_meta = test_meta(secondary_created);
3055        secondary_meta.set_provenance(family_start);
3056        secondary_meta.set_variance_key(secondary_variance);
3057
3058        let mut cache = cache_with_lookup_storage(CacheKey::new("valid-after-provenance", ""));
3059        assert!(!cache.cache_vary_lookup(secondary_variance, &primary_meta));
3060        assert_eq!(
3061            cache.inner_enabled().valid_after,
3062            Some(primary_meta.provenance())
3063        );
3064
3065        let secondary_key = cache.cache_key().to_compact();
3066        ONE_SHOT_LOOKUP_STORAGE
3067            .entries
3068            .lock()
3069            .unwrap()
3070            .push((secondary_key, secondary_meta));
3071
3072        assert!(cache.cache_lookup().await.unwrap().is_some());
3073    }
3074}