v0.34.0_2

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 23:37:43 +02:00
1 parent 0a8bc6f040
commit 434e789144
38 files changed
+899 -238

No files matched your search

+14 -13
View File
@@ -6,9 +6,6 @@ impl Cache {
.unwrap_or_else(|e| e.into_inner())
.clone();
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
self.counters
.expired
.fetch_add(store.sweep() as u64, Ordering::Relaxed);
let n = |value: &AtomicU64| value.load(Ordering::Relaxed);
let hits = n(&self.counters.hits);
let misses = n(&self.counters.misses);
@@ -180,6 +177,7 @@ impl Cache {
store.revision = revision;
self.revision.store(revision, Ordering::Relaxed);
store.domains.clear();
store.domain_count = 0;
}
drop(store);
self.cancel_all_fill_flights();
@@ -247,9 +245,6 @@ impl Cache {
}
pub fn scope_view(&self, scope: &str) -> ScopeView {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
self.counters
.expired
.fetch_add(store.sweep() as u64, Ordering::Relaxed);
let mut view = ScopeView {
entries: 0,
resident_bytes: 0,
@@ -276,26 +271,32 @@ impl Cache {
"MISS" => c.misses += 1,
_ => c.bypasses += 1,
}
*c.reasons.entry(reason.into()).or_default() += 1;
if let Some(count) = c.reasons.get_mut(reason) {
*count = count.saturating_add(1);
} else {
c.reasons.insert(reason.to_owned(), 1);
}
}
}
pub fn record_reason(&self, scope: &str, domain: &str, reason: &'static str) {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = store.domain(scope, domain) {
*c.reasons.entry(reason.into()).or_default() += 1;
if let Some(count) = c.reasons.get_mut(reason) {
*count = count.saturating_add(1);
} else {
c.reasons.insert(reason.to_owned(), 1);
}
}
}
pub fn dashboard(&self, scope: Option<&str>, offset: usize, limit: usize) -> Dashboard {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
self.counters
.expired
.fetch_add(store.sweep() as u64, Ordering::Relaxed);
let revision = store.revision;
let mut groups: BTreeMap<(String, String), DomainView> = store
.domains
.iter()
.filter(|((vhost, _), _)| scope.is_none_or(|s| s == vhost))
.map(|((vhost, domain), counters)| {
.filter(|(vhost, _)| scope.is_none_or(|s| s == vhost.as_str()))
.flat_map(|(vhost, domains)| domains.iter().map(move |(domain, counters)| (vhost, domain, counters)))
.map(|(vhost, domain, counters)| {
(
(vhost.clone(), domain.clone()),
DomainView {
+1 -1
View File
@@ -160,7 +160,7 @@ impl Candidate {
created_at,
body,
body_allocated: body_allocation,
headers: self.headers,
headers: Arc::new(self.headers),
received: self.plan.received,
last_access: Instant::now(),
resident_since: Instant::now(),
+27 -11
View File
@@ -1,17 +1,24 @@
impl Cache {
fn active_bans(&self) -> Vec<BanRule> {
if self.ban_rule_count.load(Ordering::Acquire) == 0 {
return Vec::new();
}
let now = SystemTime::now();
let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner());
bans.retain(|rule| !rule.expired(now));
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
bans.clone()
}
fn hot_entry_banned(&self, key: &Key, entry: &Entry) -> bool {
if self.ban_rule_count.load(Ordering::Acquire) == 0 {
return false;
}
let now = SystemTime::now();
let banned = self
.active_bans()
.iter()
.any(|rule| rule.matches_hot(key, entry, now));
let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner());
bans.retain(|rule| !rule.expired(now));
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
let banned = bans.iter().any(|rule| rule.matches_hot(key, entry, now));
if banned {
self.counters.ban_matches.fetch_add(1, Ordering::Relaxed);
}
@@ -19,11 +26,14 @@ impl Cache {
}
fn cold_entry_banned(&self, key: &Key, entry: &ColdEntry) -> bool {
if self.ban_rule_count.load(Ordering::Acquire) == 0 {
return false;
}
let now = SystemTime::now();
let banned = self
.active_bans()
.iter()
.any(|rule| rule.matches_cold(key, entry, now));
let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner());
bans.retain(|rule| !rule.expired(now));
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
let banned = bans.iter().any(|rule| rule.matches_cold(key, entry, now));
if banned {
self.counters.ban_matches.fetch_add(1, Ordering::Relaxed);
}
@@ -31,10 +41,14 @@ impl Cache {
}
fn entry_banned_before_insert(&self, key: &Key, entry: &Entry) -> bool {
if self.ban_rule_count.load(Ordering::Acquire) == 0 {
return false;
}
let now = SystemTime::now();
self.active_bans()
.iter()
.any(|rule| rule.matches_hot(key, entry, now))
let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner());
bans.retain(|rule| !rule.expired(now));
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
bans.iter().any(|rule| rule.matches_hot(key, entry, now))
}
pub fn bans(&self) -> Vec<BanView> {
@@ -46,6 +60,7 @@ impl Cache {
let before = bans.len();
bans.retain(|rule| rule.id != id);
let removed = bans.len() != before;
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
if removed {
self.counters.bans_removed.fetch_add(1, Ordering::Relaxed);
}
@@ -82,6 +97,7 @@ impl Cache {
created_at: now,
expires_at: now.checked_add(Duration::from_secs(ttl)).unwrap_or(now),
});
self.ban_rule_count.store(bans.len() as u64, Ordering::Release);
self.counters.bans_created.fetch_add(1, Ordering::Relaxed);
Ok(InvalidationResult {
operation: request.operation,
+2
View File
@@ -25,6 +25,7 @@ impl Cache {
fill_flight_sequence: AtomicU64::new(0),
collapse_waiters: AtomicU64::new(0),
bans: Mutex::new(Vec::new()),
ban_rule_count: AtomicU64::new(0),
ban_sequence: AtomicU64::new(0),
proactive_scheduled_keys: Mutex::new(HashSet::new()),
proactive_inflight: AtomicU64::new(0),
@@ -75,6 +76,7 @@ impl Cache {
fill_flight_sequence: AtomicU64::new(0),
collapse_waiters: AtomicU64::new(0),
bans: Mutex::new(Vec::new()),
ban_rule_count: AtomicU64::new(0),
ban_sequence: AtomicU64::new(0),
proactive_scheduled_keys: Mutex::new(HashSet::new()),
proactive_inflight: AtomicU64::new(0),
+49 -57
View File
@@ -1,29 +1,20 @@
impl Cache {
fn get_hot(&self, ticket: &Ticket, count_miss: bool) -> Option<Hit> {
// Only metadata, hit counters and exact LRU ordering require the store
// mutex. Materializing an HTTP response may allocate (headers, Body,
// Age formatting) and must be done *after* dropping that mutex.
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
let expired = store.sweep();
self.counters
.expired
.fetch_add(expired as u64, Ordering::Relaxed);
if store.revision != ticket.revision
|| store.generation != ticket.generation
|| store
.scope_generations
.get(&ticket.scope)
.copied()
.unwrap_or(0)
|| store.scope_generations.get(&ticket.scope).copied().unwrap_or(0)
!= ticket.scope_generation
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0) != ticket.key_generation
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0)
!= ticket.key_generation
{
return None;
}
if let Some(storage_key) = matching_hot_key(&store, ticket) {
// Don't remove and reinsert the entire Entry + variant index on
// every HOT hit. A cache hit changes only the LRU sequence, hits
// counter, and last-access clock; body/headers remain immutable.
let banned = store
.entries
.get(&storage_key)
let banned = store.entries.get(&storage_key)
.is_some_and(|entry| self.hot_entry_banned(&storage_key, entry));
if banned {
store.remove(&storage_key);
@@ -34,14 +25,9 @@ impl Cache {
release_allocator_memory();
return None;
}
let Some(entry) = store.entries.get(&storage_key) else {
return None;
};
let entry = store.entries.get(&storage_key)?;
let resident_age_seconds = hot_resident_age(entry).as_secs();
let age = entry
.initial_age
.saturating_add(entry.received.elapsed())
.as_secs();
let age = entry.initial_age.saturating_add(entry.received.elapsed()).as_secs();
let state = cache_state(
age,
entry.ttl,
@@ -53,48 +39,54 @@ impl Cache {
);
if state.servable() {
let sequence = store.next_sequence();
let (old_sequence, scope, domain, body_bytes, result) = {
let (old_sequence, headers, body, hits, transformed, skip_compression,
remaining_ttl, body_bytes) = {
let entry = store.entries.get_mut(&storage_key)?;
let old_sequence = entry.sequence;
entry.hits = entry.hits.saturating_add(1);
entry.last_access = Instant::now();
entry.sequence = sequence;
let mut response = Response::new(Body::from(entry.body.clone()));
*response.headers_mut() = entry.headers.clone();
if entry.transformed {
response
.extensions_mut()
.insert(crate::transform::CachedTransformed);
}
if entry.skip_compression {
response.extensions_mut().insert(crate::compression::Skip);
}
response.headers_mut().insert(
"age",
HeaderValue::from_str(&age.to_string()).expect("integer header"),
);
let result = Hit {
response,
body: entry.body.clone(),
hits: entry.hits,
transformed: entry.transformed,
remaining_ttl: entry.ttl.saturating_sub(age),
resident_age_seconds,
stale: state.stale(),
background_revalidate: state.background_revalidate(),
};
(old_sequence, entry.scope.clone(), entry.domain.clone(), entry.body.len(), result)
(
old_sequence,
Arc::clone(&entry.headers),
entry.body.clone(),
entry.hits,
entry.transformed,
entry.skip_compression,
entry.ttl.saturating_sub(age),
entry.body.len(),
)
};
store.lru.remove(&old_sequence);
store.lru.insert(sequence, storage_key);
// `store.bytes`, `store.logical_bytes` and the Vary schema index
// did not change. In particular, this path does not allocate a
// replacement variants Vec for every response.
if let Some(counter) = store.domain(&scope, &domain) {
counter.saved_body_bytes = counter.saved_body_bytes.saturating_add(body_bytes as u64);
}
// Preserve the exact LRU and domain-byte counters, without
// allocating a replacement variant index or String keys.
store.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
self.counters.hits.fetch_add(1, Ordering::Relaxed);
return Some(result);
drop(store);
let mut response = Response::new(Body::from(body.clone()));
*response.headers_mut() = headers.as_ref().clone();
if transformed {
response.extensions_mut().insert(crate::transform::CachedTransformed);
}
if skip_compression {
response.extensions_mut().insert(crate::compression::Skip);
}
response.headers_mut().insert(
"age",
HeaderValue::from_str(&age.to_string()).expect("integer header"),
);
return Some(Hit {
response,
body,
hits,
transformed,
remaining_ttl,
resident_age_seconds,
stale: state.stale(),
background_revalidate: state.background_revalidate(),
});
}
}
if count_miss {
@@ -243,7 +235,7 @@ impl Cache {
target: entry.target.clone(),
tags: entry.tags.clone(),
created_at: entry.created_at,
headers: metadata.headers.clone(),
headers: Arc::new(metadata.headers.clone()),
body: body.clone(),
body_allocated,
received: Instant::now(),
-3
View File
@@ -51,9 +51,6 @@ impl Cache {
store.remove(&key);
}
store.remove(&storage_key);
self.counters
.expired
.fetch_add(store.sweep() as u64, Ordering::Relaxed);
let revision = store.revision;
let generation = store.generation;
let mut released_hot = false;
+1 -1
View File
@@ -77,7 +77,7 @@ impl Cache {
target: ticket.target.clone(),
tags,
created_at: ticket.created_at,
headers: headers.clone(),
headers: Arc::new(headers.clone()),
body: cached.body.clone(),
body_allocated: cached.body_allocated,
received: plan.received,
+20 -32
View File
@@ -111,7 +111,7 @@ struct Entry {
target: String,
tags: Vec<String>,
created_at: SystemTime,
headers: HeaderMap,
headers: Arc<HeaderMap>,
body: Bytes,
/// Backing allocation retained by `body`; can exceed `body.len()`.
body_allocated: usize,
@@ -181,6 +181,9 @@ fn hot_entry_cost(
.sum::<usize>();
body_allocated
.saturating_add(std::mem::size_of::<Entry>())
// Arc<HeaderMap> owns the actual map on a separate allocation.
.saturating_add(std::mem::size_of::<HeaderMap>())
.saturating_add(2 * std::mem::size_of::<usize>())
.saturating_add(index_heap)
.saturating_add(header_heap)
.saturating_add(string_vec_heap(vary))
@@ -333,8 +336,8 @@ struct Store {
control_revision: u64,
scope_generations: HashMap<String, u64>,
key_generations: HashMap<Key, u64>,
domains: BTreeMap<(String, String), DomainCounters>,
last_sweep: Option<Instant>,
domains: BTreeMap<String, BTreeMap<String, DomainCounters>>,
domain_count: usize,
}
impl Store {
fn index_variant(&mut self, base_key: Key, key: Key) {
@@ -360,39 +363,24 @@ impl Store {
Some(entry)
}
fn domain(&mut self, scope: &str, domain: &str) -> Option<&mut DomainCounters> {
let key = (scope.to_owned(), domain.to_owned());
// Cardinality is bounded independently of resident cache capacity.
if self.domains.len() >= 4096 && !self.domains.contains_key(&key) {
// The common path is a borrowed lookup: no allocation of scope/domain
// strings for every cache hit or log-reason counter update.
if self.domains.get(scope).is_some_and(|d| d.contains_key(domain)) {
return self.domains.get_mut(scope)?.get_mut(domain);
}
if self.domain_count >= 4096 {
return None;
}
Some(self.domains.entry(key).or_default())
self.domain_count += 1;
Some(self.domains.entry(scope.to_owned()).or_default()
.entry(domain.to_owned()).or_default())
}
fn sweep(&mut self) -> usize {
if self
.last_sweep
.is_some_and(|v| v.elapsed() < Duration::from_secs(1))
{
return 0;
fn saved_body_hit(&mut self, scope: &str, domain: &str, bytes: usize) {
// Every admitted HOT insertion registered its domain. If the bounded
// stats table is full, skip accounting for an unregistered domain.
if let Some(c) = self.domains.get_mut(scope).and_then(|d| d.get_mut(domain)) {
c.saved_body_bytes = c.saved_body_bytes.saturating_add(bytes as u64);
}
self.last_sweep = Some(Instant::now());
let stale: Vec<_> = self
.entries
.iter()
.filter(|(_, e)| {
let age = e.initial_age.saturating_add(e.received.elapsed()).as_secs();
age >= retention_deadline(
e.ttl,
e.stale_while_revalidate,
e.stale_if_error,
e.origin_etag.is_some() || e.origin_last_modified.is_some(),
)
})
.map(|(k, _)| *k)
.collect();
for key in &stale {
self.remove(key);
}
stale.len()
}
fn next_sequence(&mut self) -> u64 {
// No practical wrapping, but a wrap must not corrupt LRU bookkeeping.
+4
View File
@@ -73,6 +73,7 @@ pub struct Cache {
fill_flight_sequence: AtomicU64,
collapse_waiters: AtomicU64,
bans: Mutex<Vec<BanRule>>,
ban_rule_count: AtomicU64,
ban_sequence: AtomicU64,
proactive_scheduled_keys: Mutex<HashSet<Key>>,
proactive_inflight: AtomicU64,
@@ -347,6 +348,9 @@ fn hot_resident_age(entry: &Entry) -> Duration {
}
fn matching_hot_key(store: &Store, ticket: &Ticket) -> Option<Key> {
if store.entries.get(&ticket.key).is_some_and(|entry| entry.vary.is_empty()) {
return Some(ticket.key);
}
store
.variants
.get(&ticket.key)?
+3
View File
@@ -19,3 +19,6 @@ include!("tests/cold_tier_tests.rs");
#[cfg(test)]
include!("tests/http_invalidation_tests.rs");
#[cfg(test)]
include!("tests/hot_hit_034_tests.rs");
-1
View File
@@ -84,7 +84,6 @@ mod dashboard_tests {
for entry in store.entries.values_mut() {
entry.received = Instant::now() - Duration::from_secs(2 * 60 * 60);
}
store.last_sweep = None;
}
let dashboard = cache.dashboard(None, 0, 50);
assert_eq!(dashboard.matching_objects, 1);
+172
View File
@@ -0,0 +1,172 @@
mod hot_hit_034_tests {
use super::*;
use http_body_util::BodyExt;
fn settings() -> Settings {
Settings { enabled: true, ..Settings::default() }
}
async fn publish(
cache: &Arc<Cache>,
settings: &Settings,
path: &str,
body: &'static str,
transformed: bool,
) -> Ticket {
let ticket = cache.ticket_scoped(
34, "app", "http", "cache.example", "origin@127.0.0.1:8080",
path, &HeaderMap::new(),
).expect("cache ticket");
let mut response = Response::new(Body::from(body));
response.headers_mut().insert(
"cache-control", HeaderValue::from_static("public, max-age=31536000"),
);
response.headers_mut().insert(
"etag", HeaderValue::from_static("\"cached-representation\""),
);
if transformed {
response.extensions_mut().insert(crate::transform::Applied {
kind: crate::transform::Kind::Css,
original_bytes: body.len(),
transformed_bytes: body.len(),
});
}
let policy = plan(StatusCode::OK, response.headers(), settings, Duration::ZERO)
.expect("public static object cacheable");
cache.wrap(response, ticket.clone(), policy, settings)
.into_body().collect().await.unwrap();
ticket
}
#[tokio::test]
async fn hot_hit_headers_are_independent_and_lru_counters_remain_exact() {
let cache = Cache::new(34);
let settings = settings();
let ticket = publish(&cache, &settings, "/theme.css", "body{color:red}", true).await;
let initial = cache.view();
let before = cache.dashboard(Some("app"), 0, 10);
assert_eq!(before.matching_objects, 1);
assert!(before.objects[0].transformed);
for i in 1..=500 {
let hit = cache.get(&ticket).expect("HOT cache hit");
assert_eq!(hit.hits, i);
assert!(hit.transformed);
assert!(hit.response.extensions().get::<crate::transform::CachedTransformed>().is_some());
assert_eq!(hit.response.headers()["etag"], "\"cached-representation\"");
let mut independent = hit.response;
independent.headers_mut().insert("etag", HeaderValue::from_static("\"client-only\""));
if i == 500 {
assert_eq!(independent.into_body().collect().await.unwrap().to_bytes(), Bytes::from_static(b"body{color:red}"));
}
}
let after = cache.view();
assert_eq!(after.entries, 1);
assert_eq!(after.hot_allocated_bytes, initial.hot_allocated_bytes);
assert_eq!(after.hot_logical_bytes, initial.hot_logical_bytes);
assert_eq!(after.hits.saturating_sub(initial.hits), 500);
let store = cache.store.lock().unwrap();
assert_eq!(store.entries.len(), 1);
assert_eq!(store.lru.len(), 1);
assert_eq!(store.variants.len(), 1);
drop(store);
let dashboard = cache.dashboard(Some("app"), 0, 10);
assert_eq!(dashboard.objects[0].hits, 500);
assert_eq!(dashboard.domains[0].counters.saved_body_bytes, 500 * 15);
assert_eq!(cache.get(&ticket).unwrap().response.headers()["etag"], "\"cached-representation\"");
}
#[tokio::test]
async fn concurrent_hot_hits_have_unique_monotonic_counts_and_bounded_indexes() {
let cache = Cache::new(34);
let ticket = publish(&cache, &settings(), "/shared.css", "cachebody", false).await;
let workers = 6;
let per_worker = 300;
let mut threads = Vec::new();
for _ in 0..workers {
let cache = Arc::clone(&cache);
let ticket = ticket.clone();
threads.push(std::thread::spawn(move || {
let mut hits = Vec::with_capacity(per_worker);
for _ in 0..per_worker {
let hit = cache.get(&ticket).expect("concurrent HOT hit");
assert_eq!(hit.response.headers()["etag"], "\"cached-representation\"");
hits.push(hit.hits);
}
hits
}));
}
let mut counts: Vec<_> = threads.into_iter().flat_map(|t| t.join().unwrap()).collect();
counts.sort_unstable();
assert_eq!(counts, (1..=(workers * per_worker) as u64).collect::<Vec<_>>());
assert_eq!(cache.view().hits, (workers * per_worker) as u64);
let store = cache.store.lock().unwrap();
assert_eq!(store.lru.len(), 1);
assert_eq!(store.entries.len(), 1);
}
#[tokio::test]
async fn hot_lru_eviction_and_ban_are_preserved() {
let cache = Cache::new(34);
let settings = Settings { max_entries: 2, ..settings() };
let a = publish(&cache, &settings, "/a.css", "AAAA", false).await;
let b = publish(&cache, &settings, "/b.css", "BBBB", false).await;
assert!(cache.get(&a).is_some(), "A becomes most recently used");
let c = publish(&cache, &settings, "/c.css", "CCCC", false).await;
assert!(cache.get(&a).is_some());
assert!(cache.get(&b).is_none(), "B is least recently used");
assert!(cache.get(&c).is_some());
let ban = cache.invalidate(InvalidationRequest {
operation: InvalidationOperation::Ban,
selector: InvalidationSelector::Exact,
value: "/a.css".into(),
host: Some("cache.example".into()),
vhost: Some("app".into()),
ttl_seconds: Some(3600),
confirm_wide: false,
}).unwrap();
assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 1);
assert!(cache.get(&a).is_none(), "BAN must block cached A");
assert!(cache.remove_ban(ban.ban_id.unwrap()));
assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 0);
assert!(cache.get(&c).is_some(), "unrelated C must remain cached");
}
#[tokio::test]
#[ignore = "manual in-process timing only; NOT HTTP RPS"]
async fn manual_hot_lookup_microbenchmark() {
let cache = Cache::new(34);
let ticket = publish(&cache, &settings(), "/bench.css", "smallbody", true).await;
for _ in 0..1_000 {
std::hint::black_box(cache.get(&ticket).unwrap());
}
let iterations = 100_000;
let start = Instant::now();
for _ in 0..iterations {
std::hint::black_box(cache.get(&ticket).unwrap());
}
let duration = start.elapsed();
eprintln!("HOT lookup incl. response materialization: {:.1} ns/op ({} hits); NOT network RPS",
duration.as_secs_f64() * 1e9 / iterations as f64, iterations);
assert_eq!(cache.view().hits, iterations + 1_000);
}
#[tokio::test]
async fn stale_object_remains_available_for_explicit_max_stale() {
let cache = Cache::new(34);
let ticket = publish(&cache, &settings(), "/old.css", "oldbody", false).await;
{
let mut store = cache.store.lock().unwrap();
store.entries.values_mut().next().unwrap().received =
Instant::now() - Duration::from_secs(700_000);
}
assert!(cache.get(&ticket).is_none(), "expired freshness is not an unconditional HIT");
assert_eq!(cache.view().entries, 1, "stale representation must survive for revalidation");
let mut stale_ticket = ticket;
stale_ticket.request_headers.insert(
"cache-control", HeaderValue::from_static("max-stale"),
);
assert!(cache.get(&stale_ticket).unwrap().stale);
}
}
+1 -5
View File
@@ -109,10 +109,6 @@ impl Cache {
}
pub fn revalidation(&self, ticket: &Ticket) -> Option<Revalidation> {
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
let expired = store.sweep();
self.counters
.expired
.fetch_add(expired as u64, Ordering::Relaxed);
if store.revision != ticket.revision
|| store.generation != ticket.generation
|| store
@@ -142,7 +138,7 @@ impl Cache {
.saturating_add(entry.received.elapsed())
.as_secs();
Some(Revalidation {
headers: entry.headers.clone(),
headers: entry.headers.as_ref().clone(),
body: entry.body.clone(),
body_allocated: entry.body_allocated,
_transient_leases: Vec::new(),