This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 09:29:10 +02:00
1 parent 2e4aa4df4d
commit 87e571c170
106 files changed
+17952 -6185

No files matched your search

+84 -34
View File
@@ -33,7 +33,10 @@ impl Cache {
for item in read_dir.flatten() {
let path = item.path();
if path.is_file() {
let extension = path.extension().and_then(|v| v.to_str()).unwrap_or_default();
let extension = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or_default();
if matches!(extension, "json" | "body" | "tmp") {
let _ = std::fs::remove_file(path);
}
@@ -142,12 +145,24 @@ impl Cache {
revision,
scope: file.scope,
scheme: file.scheme,
authority: if file.authority.is_empty() { file.domain.clone() } else { file.authority },
authority: if file.authority.is_empty() {
file.domain.clone()
} else {
file.authority
},
domain: file.domain,
path: file.path.clone(),
target: if file.target.is_empty() { file.path } else { file.target },
target: if file.target.is_empty() {
file.path
} else {
file.target
},
tags: file.tags,
created_at: if file.created_at_ms == 0 { stored_at } else { system_time_ms(file.created_at_ms) },
created_at: if file.created_at_ms == 0 {
stored_at
} else {
system_time_ms(file.created_at_ms)
},
headers,
body_path,
meta_path: path,
@@ -310,7 +325,9 @@ impl Cache {
let key = key_hex(&draft.key);
let body_name = format!("{key}.body");
let (body_path, meta_path, body_tmp, meta_tmp) = cold_paths(&draft.root, &draft.key);
let Some(shard) = body_path.parent() else { return; };
let Some(shard) = body_path.parent() else {
return;
};
if tokio::fs::create_dir_all(shard).await.is_err() {
self.counters.cold_errors.fetch_add(1, Ordering::Relaxed);
return;
@@ -334,7 +351,13 @@ impl Cache {
path: draft.entry.path.clone(),
target: draft.entry.target.clone(),
tags: draft.entry.tags.clone(),
created_at_ms: draft.entry.created_at.duration_since(UNIX_EPOCH).unwrap_or_default().as_millis().min(u64::MAX as u128) as u64,
created_at_ms: draft
.entry
.created_at
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(u64::MAX as u128) as u64,
headers,
body_file: body_name,
body_bytes: draft.entry.body.len(),
@@ -347,8 +370,18 @@ impl Cache {
stale_if_error: draft.entry.stale_if_error,
must_revalidate: draft.entry.must_revalidate,
proxy_revalidate: draft.entry.proxy_revalidate,
origin_etag: draft.entry.origin_etag.as_ref().and_then(|v| v.to_str().ok()).map(str::to_owned),
origin_last_modified: draft.entry.origin_last_modified.as_ref().and_then(|v| v.to_str().ok()).map(str::to_owned),
origin_etag: draft
.entry
.origin_etag
.as_ref()
.and_then(|v| v.to_str().ok())
.map(str::to_owned),
origin_last_modified: draft
.entry
.origin_last_modified
.as_ref()
.and_then(|v| v.to_str().ok())
.map(str::to_owned),
last_access_ms: now_ms(),
hits: draft.entry.hits,
transformed: draft.entry.transformed,
@@ -592,8 +625,19 @@ impl Cache {
.get(&entry.scope)
.copied()
.unwrap_or(0);
let key_generation = store.key_generations.get(&entry.base_key).copied().unwrap_or(0);
if !self.enqueue_demotion(key, entry, revision, generation, scope_generation, key_generation) {
let key_generation = store
.key_generations
.get(&entry.base_key)
.copied()
.unwrap_or(0);
if !self.enqueue_demotion(
key,
entry,
revision,
generation,
scope_generation,
key_generation,
) {
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
}
}
@@ -633,18 +677,25 @@ impl Cache {
.get(&entry.scope)
.copied()
.unwrap_or(0);
let key_generation = store.key_generations.get(&entry.base_key).copied().unwrap_or(0);
let key_generation = store
.key_generations
.get(&entry.base_key)
.copied()
.unwrap_or(0);
let bytes = entry.cost;
let memory_lease = self.try_reserve_transient(bytes, &settings).unwrap_or_else(|| {
let bytes = bytes as u64;
self.transient_bytes_active.fetch_add(bytes, Ordering::Relaxed);
TransientLease {
_inner: Arc::new(TransientLeaseInner {
counter: self.transient_bytes_active.clone(),
bytes,
}),
}
});
let memory_lease =
self.try_reserve_transient(bytes, &settings)
.unwrap_or_else(|| {
let bytes = bytes as u64;
self.transient_bytes_active
.fetch_add(bytes, Ordering::Relaxed);
TransientLease {
_inner: Arc::new(TransientLeaseInner {
counter: self.transient_bytes_active.clone(),
bytes,
}),
}
});
ColdDraft {
key,
revision,
@@ -780,7 +831,10 @@ impl Cache {
let batch: Vec<_> = {
let cold = self.cold.lock().unwrap_or_else(|e| e.into_inner());
cold.lru
.range((std::ops::Bound::Excluded(after_sequence), std::ops::Bound::Unbounded))
.range((
std::ops::Bound::Excluded(after_sequence),
std::ops::Bound::Unbounded,
))
.take(SNAPSHOT_BATCH)
.filter_map(|(sequence, key)| {
cold.entries.get(key).map(|entry| {
@@ -860,11 +914,9 @@ impl Cache {
break;
}
}
if !cold
.entries
.get(&candidate.key)
.is_some_and(|entry| entry.sequence == candidate.sequence && entry.pinned_reads == 0)
{
if !cold.entries.get(&candidate.key).is_some_and(|entry| {
entry.sequence == candidate.sequence && entry.pinned_reads == 0
}) {
continue;
}
if let Some(entry) = cold.remove(&candidate.key) {
@@ -940,7 +992,10 @@ impl Cache {
.clone()
.try_acquire_owned()
.map_err(|_| anyhow::anyhow!("cold cache cleanup is already running"))?;
let id = self.cleanup_sequence.fetch_add(1, Ordering::Relaxed).saturating_add(1);
let id = self
.cleanup_sequence
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1);
let job = ColdCleanupJob {
id,
status: "queued",
@@ -974,12 +1029,7 @@ impl Cache {
.unwrap_or_else(|e| e.into_inner())
.clone();
let result = cache
.remove_cold_candidates(
Some(&request),
None,
None,
settings.cold_gc_max_delete,
)
.remove_cold_candidates(Some(&request), None, None, settings.cold_gc_max_delete)
.await;
let mut jobs = cache.cleanup_jobs.lock().unwrap_or_else(|e| e.into_inner());
if let Some(job) = jobs.get_mut(&id) {
+10 -28
View File
@@ -36,10 +36,7 @@ impl Drop for CollapseWaiterGuard {
impl Drop for FillLeaderGuard {
fn drop(&mut self) {
if let Some(cache) = self.cache.upgrade() {
let mut flights = cache
.fill_flights
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut flights = cache.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
let current = flights
.get(&self.key)
.is_some_and(|flight| flight.id == self.id);
@@ -53,19 +50,12 @@ impl Drop for FillLeaderGuard {
}
impl Cache {
pub async fn claim_fill(
self: &Arc<Self>,
ticket: &Ticket,
settings: &Settings,
) -> FillClaim {
pub async fn claim_fill(self: &Arc<Self>, ticket: &Ticket, settings: &Settings) -> FillClaim {
if !settings.collapse_enabled {
return FillClaim::Disabled;
}
let existing = {
let mut flights = self
.fill_flights
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
if let Some(flight) = flights.get(&ticket.key) {
Some(flight.clone())
} else {
@@ -131,11 +121,11 @@ impl Cache {
fn cancel_all_fill_flights(&self) {
let flights = {
let mut flights = self
.fill_flights
.lock()
.unwrap_or_else(|e| e.into_inner());
flights.drain().map(|(_, flight)| flight).collect::<Vec<_>>()
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
flights
.drain()
.map(|(_, flight)| flight)
.collect::<Vec<_>>()
};
for flight in flights {
flight.done.send_replace(true);
@@ -144,10 +134,7 @@ impl Cache {
fn cancel_scope_fill_flights(&self, scope: &str) {
let removed = {
let mut flights = self
.fill_flights
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
let keys = flights
.iter()
.filter(|(_, flight)| flight.scope == scope)
@@ -250,12 +237,7 @@ impl Cache {
}
if self
.proactive_inflight
.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Relaxed,
)
.compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
self.counters
+21 -5
View File
@@ -102,14 +102,22 @@ impl Cache {
("bans_removed_total", "counter", view.bans_removed),
("ban_matches_total", "counter", view.ban_matches),
("active_bans", "gauge", view.active_bans as u64),
("proactive_scheduled_total", "counter", view.proactive_scheduled),
(
"proactive_scheduled_total",
"counter",
view.proactive_scheduled,
),
("proactive_started_total", "counter", view.proactive_started),
("proactive_skipped_total", "counter", view.proactive_skipped),
("proactive_inflight", "gauge", view.proactive_inflight),
("entries", "gauge", view.entries as u64),
("resident_bytes", "gauge", view.resident_bytes as u64),
("hot_logical_bytes", "gauge", view.hot_logical_bytes as u64),
("hot_allocated_bytes", "gauge", view.hot_allocated_bytes as u64),
(
"hot_allocated_bytes",
"gauge",
view.hot_allocated_bytes as u64,
),
("hot_limit_bytes", "gauge", view.hot_limit_bytes as u64),
(
"transient_limit_bytes",
@@ -124,7 +132,11 @@ impl Cache {
("cold_entries", "gauge", view.cold_entries as u64),
("cold_bytes", "gauge", view.cold_bytes),
("cold_limit_bytes", "gauge", view.cold_limit_bytes),
("cold_limit_entries", "gauge", view.cold_limit_entries as u64),
(
"cold_limit_entries",
"gauge",
view.cold_limit_entries as u64,
),
("cold_hits_total", "counter", view.cold_hits),
("cold_promotions_total", "counter", view.cold_promotions),
("hot_demotions_total", "counter", view.hot_demotions),
@@ -201,7 +213,9 @@ impl Cache {
let _ = tx.try_send(ColdCommand::PurgeDirectory(root));
}
self.counters.purges.fetch_add(1, Ordering::Relaxed);
self.counters.purge_entries.fetch_add(count as u64, Ordering::Relaxed);
self.counters
.purge_entries
.fetch_add(count as u64, Ordering::Relaxed);
count
}
/// Purge one vhost without evicting other hosts. Generation bumps also stop
@@ -242,7 +256,9 @@ impl Cache {
let _ = tx.try_send(ColdCommand::Delete(delete));
}
self.counters.purges.fetch_add(1, Ordering::Relaxed);
self.counters.purge_entries.fetch_add(count as u64, Ordering::Relaxed);
self.counters
.purge_entries
.fetch_add(count as u64, Ordering::Relaxed);
count
}
pub fn scope_view(&self, scope: &str) -> ScopeView {
+2 -2
View File
@@ -233,7 +233,8 @@ impl HttpBody for FillBody {
this.candidate = None;
} else {
let after = candidate.data.capacity();
let unbudgeted = after.saturating_sub(before.saturating_add(required_growth));
let unbudgeted =
after.saturating_sub(before.saturating_add(required_growth));
if unbudgeted > 0
&& !candidate.fill_gauge.grow(unbudgeted, &candidate.settings)
{
@@ -340,4 +341,3 @@ impl Drop for FillBody {
}
}
}
+80 -18
View File
@@ -84,6 +84,7 @@ impl Cache {
_ = maintenance_shutdown.cancelled() => break,
_ = tick.tick() => {
let Some(cache) = weak.upgrade() else { break; };
cache.trim_hot_idle();
cache.demote_idle();
cache.sweep_cold().await;
cache.gc_banned(256);
@@ -94,7 +95,9 @@ impl Cache {
let weak = Arc::downgrade(&cache);
tokio::spawn(async move {
loop {
let Some(cache) = weak.upgrade() else { break; };
let Some(cache) = weak.upgrade() else {
break;
};
let interval = cache
.tier_settings
.lock()
@@ -156,8 +159,8 @@ impl Cache {
}
let lease = self.try_reserve_transient(bytes, settings)?;
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
let fill_now = usize::try_from(self.fill_bytes_active.load(Ordering::Relaxed))
.unwrap_or(usize::MAX);
let fill_now =
usize::try_from(self.fill_bytes_active.load(Ordering::Relaxed)).unwrap_or(usize::MAX);
let allowed = settings
.max_bytes
.saturating_add(Self::fill_headroom_bytes(settings.max_bytes));
@@ -165,20 +168,14 @@ impl Cache {
let generation = store.generation;
let mut removed = 0usize;
let mut released_hot = false;
while store
.bytes
.saturating_add(fill_now)
.saturating_add(bytes)
> allowed
{
while store.bytes.saturating_add(fill_now).saturating_add(bytes) > allowed {
if removed >= settings.hot_eviction_max_objects {
self.counters
.hot_eviction_limited
.fetch_add(1, Ordering::Relaxed);
break;
}
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key))
else {
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
break;
};
let Some(old) = store.remove(&key) else {
@@ -210,11 +207,7 @@ impl Cache {
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
}
}
let fits = store
.bytes
.saturating_add(fill_now)
.saturating_add(bytes)
<= allowed;
let fits = store.bytes.saturating_add(fill_now).saturating_add(bytes) <= allowed;
if fits {
self.fill_bytes_active
.fetch_add(bytes as u64, Ordering::Relaxed);
@@ -234,6 +227,76 @@ impl Cache {
}
}
/// Bound long-lived HOT RSS without flushing the working set. Only entries
/// idle for the configured age are considered, and only while HOT usage is
/// above the configured target waterline.
fn trim_hot_idle(&self) {
let settings = self
.tier_settings
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if !settings.hot_auto_trim_enabled {
return;
}
let target = settings
.max_bytes
.saturating_mul(settings.hot_auto_trim_target_percent as usize)
/ 100;
let idle_cutoff = Duration::from_secs(settings.hot_auto_trim_after_seconds);
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
if store.bytes <= target {
return;
}
let revision = store.revision;
let generation = store.generation;
let mut removed = 0usize;
while store.bytes > target && removed < settings.hot_eviction_max_objects {
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
break;
};
let idle = store
.entries
.get(&key)
.is_some_and(|entry| entry.last_access.elapsed() >= idle_cutoff);
if !idle {
// LRU order is also last-access order, so newer entries cannot qualify.
break;
}
let Some(entry) = store.remove(&key) else {
continue;
};
removed += 1;
let scope_generation = store
.scope_generations
.get(&entry.scope)
.copied()
.unwrap_or(0);
let key_generation = store
.key_generations
.get(&entry.base_key)
.copied()
.unwrap_or(0);
if !(settings.cold_enabled
&& settings.cold_on_lru
&& self.enqueue_demotion(
key,
entry,
revision,
generation,
scope_generation,
key_generation,
))
{
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
}
}
drop(store);
if removed > 0 {
release_allocator_memory();
}
}
pub fn configure(&self, settings: Settings) {
*self.tier_settings.lock().unwrap_or_else(|e| e.into_inner()) = settings.clone();
// Hot-applied limit reductions take effect immediately for published HOT
@@ -244,8 +307,7 @@ impl Cache {
let generation = store.generation;
let mut released_hot = false;
while store.entries.len() > settings.max_entries || store.bytes > settings.max_bytes {
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key))
else {
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
break;
};
let Some(old) = store.remove(&key) else {
+23 -16
View File
@@ -13,12 +13,7 @@ impl Cache {
.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;
}
@@ -490,7 +485,10 @@ impl Cache {
skip_compression: parts.extensions.get::<crate::compression::Skip>().is_some(),
origin_etag,
origin_last_modified,
transformed: parts.extensions.get::<crate::transform::Applied>().is_some(),
transformed: parts
.extensions
.get::<crate::transform::Applied>()
.is_some(),
_permit: permit,
_fill_leader: fill_leader,
};
@@ -522,7 +520,10 @@ impl Cache {
}
if entry.cost > settings.max_bytes
|| (!entry.requires_revalidation
&& entry.initial_age.saturating_add(entry.received.elapsed()).as_secs()
&& entry
.initial_age
.saturating_add(entry.received.elapsed())
.as_secs()
>= retention_deadline(
entry.ttl,
entry.stale_while_revalidate,
@@ -541,12 +542,7 @@ impl Cache {
.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 false;
}
@@ -584,10 +580,21 @@ impl Cache {
.get(&old.scope)
.copied()
.unwrap_or(0);
let key_generation = store.key_generations.get(&old.base_key).copied().unwrap_or(0);
let key_generation = store
.key_generations
.get(&old.base_key)
.copied()
.unwrap_or(0);
if !(settings.cold_enabled
&& settings.cold_on_lru
&& self.enqueue_demotion(key, old, revision, generation, scope_generation, key_generation))
&& self.enqueue_demotion(
key,
old,
revision,
generation,
scope_generation,
key_generation,
))
{
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
}
+42 -16
View File
@@ -112,6 +112,12 @@ pub struct Settings {
/// Maximum number of LRU objects removed during one admission attempt.
/// Reaching the limit makes the new object pass through uncached.
pub hot_eviction_max_objects: usize,
/// Automatically release idle HOT objects once the tier is above the target waterline.
pub hot_auto_trim_enabled: bool,
/// Minimum idle age before automatic HOT trimming can evict an object.
pub hot_auto_trim_after_seconds: u64,
/// Target HOT usage percentage reached by each automatic trim pass.
pub hot_auto_trim_target_percent: u8,
/// Persistent cold tier. Disabled by default to avoid implicit disk writes.
pub cold_enabled: bool,
/// Last explicitly selected COLD policy profile, persisted with the cache
@@ -179,6 +185,9 @@ impl Default for Settings {
transient_max_bytes: 64 * 1024 * 1024,
cold_queue_max_bytes: 32 * 1024 * 1024,
hot_eviction_max_objects: 64,
hot_auto_trim_enabled: true,
hot_auto_trim_after_seconds: 300,
hot_auto_trim_target_percent: 80,
cold_enabled: false,
cold_preset: None,
cold_path: "cache/cold".into(),
@@ -261,6 +270,14 @@ impl Settings {
(1..=4096).contains(&self.hot_eviction_max_objects),
"cache.hot_eviction_max_objects must be 1..4096"
);
ensure!(
(5..=86_400).contains(&self.hot_auto_trim_after_seconds),
"cache.hot_auto_trim_after_seconds must be 5..86400"
);
ensure!(
(10..=95).contains(&self.hot_auto_trim_target_percent),
"cache.hot_auto_trim_target_percent must be 10..95"
);
ensure!(
(65_536..=1024_u64 * 1024 * 1024 * 1024).contains(&self.cold_max_bytes),
"cache.cold_max_bytes must be 64 KiB..1 TiB"
@@ -337,7 +354,10 @@ impl Settings {
(500..=599).contains(status),
"cache.stale_if_error_statuses must contain HTTP 5xx status codes"
);
ensure!(statuses.insert(*status), "duplicate stale-if-error HTTP status");
ensure!(
statuses.insert(*status),
"duplicate stale-if-error HTTP status"
);
}
ensure!(
(1..=604_800).contains(&self.proactive_refresh_before_expiry_seconds),
@@ -560,7 +580,6 @@ pub struct ScopeView {
pub cold_bytes: u64,
}
#[derive(Default)]
struct Counters {
hits: AtomicU64,
@@ -712,7 +731,11 @@ pub struct ColdCleanupRequest {
}
impl Default for ColdCleanupRequest {
fn default() -> Self {
Self { unused_for_seconds: None, oldest: Some(100), hits_zero: false }
Self {
unused_for_seconds: None,
oldest: Some(100),
hits_zero: false,
}
}
}
impl ColdCleanupRequest {
@@ -722,7 +745,10 @@ impl ColdCleanupRequest {
"at least one cold-cache cleanup criterion is required"
);
if let Some(value) = self.unused_for_seconds {
ensure!((1..=31_536_000).contains(&value), "unused_for_seconds must be 1..31536000");
ensure!(
(1..=31_536_000).contains(&value),
"unused_for_seconds must be 1..31536000"
);
}
if let Some(value) = self.oldest {
ensure!((1..=10_000).contains(&value), "oldest must be 1..10000");
@@ -1301,9 +1327,11 @@ impl Revalidation {
}
}
if let Some(Some(value)) = control.get("min-fresh") {
if value.parse::<u64>().ok().is_some_and(|required| {
self.ttl.saturating_sub(self.age) < required
}) {
if value
.parse::<u64>()
.ok()
.is_some_and(|required| self.ttl.saturating_sub(self.age) < required)
{
return Some("request_min_fresh");
}
}
@@ -1497,14 +1525,16 @@ fn default_cache_scheme() -> String {
fn cache_tags(headers: &HeaderMap) -> Vec<String> {
let mut tags = Vec::new();
for value in headers.get_all("cache-tag") {
let Ok(value) = value.to_str() else { continue; };
let Ok(value) = value.to_str() else {
continue;
};
for raw in value.split(|c: char| c == ',' || c.is_ascii_whitespace()) {
let tag = raw.trim();
if tag.is_empty()
|| tag.len() > 128
|| !tag
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/'))
|| !tag.bytes().all(|b| {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/')
})
{
continue;
}
@@ -1660,10 +1690,7 @@ fn cache_state(
requires_revalidation: bool,
request_headers: &HeaderMap,
) -> CacheState {
if age < ttl
&& !requires_revalidation
&& request_freshness_allows(age, ttl, request_headers)
{
if age < ttl && !requires_revalidation && request_freshness_allows(age, ttl, request_headers) {
return CacheState::Fresh;
}
match stale_mode(
@@ -1710,4 +1737,3 @@ fn pairs_to_headers(values: &[(String, String)]) -> Option<HeaderMap> {
}
Some(headers)
}
+315 -124
View File
@@ -172,7 +172,14 @@ mod tests {
let mut single_range = HeaderMap::new();
single_range.insert("range", HeaderValue::from_static("bytes=0-3"));
assert_eq!(
request_policy(&Method::GET, &single_range, true, "/asset.bin", false, &settings()),
request_policy(
&Method::GET,
&single_range,
true,
"/asset.bin",
false,
&settings()
),
RequestPolicy::Use
);
assert!(request_cacheable(
@@ -185,7 +192,14 @@ mod tests {
));
single_range.insert("range", HeaderValue::from_static("bytes=0-3,7-9"));
assert_eq!(
request_policy(&Method::GET, &single_range, true, "/asset.bin", false, &settings()),
request_policy(
&Method::GET,
&single_range,
true,
"/asset.bin",
false,
&settings()
),
RequestPolicy::Bypass("request_range_unsupported")
);
}
@@ -210,11 +224,23 @@ mod tests {
fn request_max_stale_is_bounded_and_respects_revalidation_flags() {
let unbounded = headers("max-stale");
let bounded = headers("max-stale=15");
assert_eq!(stale_mode(120, 60, 0, false, false, false, &unbounded), Some(false));
assert_eq!(stale_mode(70, 60, 0, false, false, false, &bounded), Some(false));
assert_eq!(
stale_mode(120, 60, 0, false, false, false, &unbounded),
Some(false)
);
assert_eq!(
stale_mode(70, 60, 0, false, false, false, &bounded),
Some(false)
);
assert_eq!(stale_mode(80, 60, 0, false, false, false, &bounded), None);
assert_eq!(stale_mode(70, 60, 30, false, false, false, &HeaderMap::new()), Some(true));
assert_eq!(stale_mode(100, 60, 30, false, false, false, &HeaderMap::new()), None);
assert_eq!(
stale_mode(70, 60, 30, false, false, false, &HeaderMap::new()),
Some(true)
);
assert_eq!(
stale_mode(100, 60, 30, false, false, false, &HeaderMap::new()),
None
);
assert_eq!(stale_mode(70, 60, 30, true, false, false, &unbounded), None);
assert_eq!(stale_mode(70, 60, 30, false, true, false, &unbounded), None);
}
@@ -260,9 +286,21 @@ mod tests {
origin_last_modified: None,
};
assert!(base.can_stale_if_error());
assert!(!Revalidation { age: 91, ..base.clone() }.can_stale_if_error());
assert!(!Revalidation { must_revalidate: true, ..base.clone() }.can_stale_if_error());
assert!(!Revalidation { proxy_revalidate: true, ..base }.can_stale_if_error());
assert!(!Revalidation {
age: 91,
..base.clone()
}
.can_stale_if_error());
assert!(!Revalidation {
must_revalidate: true,
..base.clone()
}
.can_stale_if_error());
assert!(!Revalidation {
proxy_revalidate: true,
..base
}
.can_stale_if_error());
}
#[test]
@@ -370,14 +408,10 @@ mod tests {
json: true,
..Default::default()
};
let transformed = crate::transform::apply(
response,
"/data.json",
&transform_settings,
false,
)
.await
.unwrap();
let transformed =
crate::transform::apply(response, "/data.json", &transform_settings, false)
.await
.unwrap();
let transformed_plan = plan(
StatusCode::OK,
transformed.headers(),
@@ -395,7 +429,9 @@ mod tests {
let first = wrapped.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&first[..], br#"{"message":"hello"}"#);
let hit = cache.get(&ticket).expect("transformed representation should be cached");
let hit = cache
.get(&ticket)
.expect("transformed representation should be cached");
assert_eq!(hit.response.headers()["etag"], expected_etag);
assert_eq!(
hit.response.headers()["content-length"].to_str().unwrap(),
@@ -452,10 +488,9 @@ mod tests {
"cache-control",
HeaderValue::from_static("public, max-age=60"),
);
response.headers_mut().insert(
"content-type",
HeaderValue::from_str(content_type).unwrap(),
);
response
.headers_mut()
.insert("content-type", HeaderValue::from_str(content_type).unwrap());
response.headers_mut().insert(
"content-length",
HeaderValue::from_str(&origin_body.len().to_string()).unwrap(),
@@ -488,14 +523,9 @@ mod tests {
"json" => transform_settings.json = true,
_ => unreachable!(),
}
let transformed = crate::transform::apply(
response,
path,
&transform_settings,
false,
)
.await
.unwrap();
let transformed = crate::transform::apply(response, path, &transform_settings, false)
.await
.unwrap();
let transformed_plan = plan(
StatusCode::OK,
transformed.headers(),
@@ -513,9 +543,15 @@ mod tests {
);
let first = wrapped.into_body().collect().await.unwrap().to_bytes();
assert!(!first.is_empty(), "{kind}");
assert_ne!(first.as_ref(), origin_body, "{kind} should change the chosen fixture");
assert_ne!(
first.as_ref(),
origin_body,
"{kind} should change the chosen fixture"
);
let hit = cache.get(&ticket).expect("transformed representation should be cached");
let hit = cache
.get(&ticket)
.expect("transformed representation should be cached");
assert_eq!(hit.response.headers()["etag"], expected_etag, "{kind}");
assert_eq!(
hit.response.headers()["content-length"],
@@ -693,7 +729,9 @@ mod tests {
let mut s = Settings::default();
s.transient_max_bytes = 128 * 1024;
s.cold_queue_max_bytes = 64 * 1024;
let first = cache.try_reserve_transient(96 * 1024, &s).expect("first reservation");
let first = cache
.try_reserve_transient(96 * 1024, &s)
.expect("first reservation");
assert!(cache.try_reserve_transient(40 * 1024, &s).is_none());
assert_eq!(cache.view().transient_bytes, 96 * 1024);
drop(first);
@@ -748,9 +786,7 @@ mod tests {
must_revalidate: false,
proxy_revalidate: false,
origin_etag: Some(HeaderValue::from_static("W/\"origin\"")),
origin_last_modified: Some(HeaderValue::from_static(
"Mon, 26 Jan 2026 23:11:40 GMT",
)),
origin_last_modified: Some(HeaderValue::from_static("Mon, 26 Jan 2026 23:11:40 GMT")),
};
let mut matching = HeaderMap::new();
matching.insert("if-none-match", HeaderValue::from_static("\"origin\""));
@@ -773,13 +809,7 @@ mod tests {
let mut response_headers = headers("public, max-age=60");
response_headers.insert("etag", public_etag.clone());
response_headers.insert("last-modified", modified.clone());
let plan = plan(
StatusCode::OK,
&response_headers,
&settings,
Duration::ZERO,
)
.unwrap();
let plan = plan(StatusCode::OK, &response_headers, &settings, Duration::ZERO).unwrap();
let mut response = Response::new(Body::from("transformed"));
*response.headers_mut() = response_headers;
response
@@ -810,9 +840,15 @@ mod tests {
assert_eq!(client_conditional["if-modified-since"], modified);
let mut validation = HeaderMap::new();
validation.insert("cache-control", HeaderValue::from_static("public, max-age=60"));
validation.insert(
"cache-control",
HeaderValue::from_static("public, max-age=60"),
);
validation.insert("etag", HeaderValue::from_static("\"origin-new\""));
validation.insert("last-modified", HeaderValue::from_static("Mon, 26 Jan 2026 23:12:40 GMT"));
validation.insert(
"last-modified",
HeaderValue::from_static("Mon, 26 Jan 2026 23:12:40 GMT"),
);
let merged = cached.merge_validation_headers(&validation);
assert_eq!(merged["etag"], public_etag);
}
@@ -907,18 +943,13 @@ mod tests {
let mut origin_headers = headers("public, max-age=60");
origin_headers.insert("vary", HeaderValue::from_static("Accept-Encoding"));
origin_headers.insert("content-length", HeaderValue::from_static("7"));
let origin_plan = plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO)
.expect("origin plan");
let origin_plan =
plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO).expect("origin plan");
let mut transformed_headers = headers("public, max-age=60");
transformed_headers.insert("content-length", HeaderValue::from_static("7"));
let transformed_plan = plan(
StatusCode::OK,
&transformed_headers,
&s,
Duration::ZERO,
)
.expect("transformed plan");
let transformed_plan = plan(StatusCode::OK, &transformed_headers, &s, Duration::ZERO)
.expect("transformed plan");
let cache_plan = origin_plan
.without_vary("accept-encoding")
.restrict(transformed_plan);
@@ -954,21 +985,13 @@ mod tests {
dark.insert("x-theme", HeaderValue::from_static("dark"));
let mut origin_headers = headers("public, max-age=60");
origin_headers.insert(
"vary",
HeaderValue::from_static("Accept-Encoding, X-Theme"),
);
let origin_plan = plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO)
.expect("origin plan");
origin_headers.insert("vary", HeaderValue::from_static("Accept-Encoding, X-Theme"));
let origin_plan =
plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO).expect("origin plan");
let mut transformed_headers = headers("public, max-age=60");
transformed_headers.insert("vary", HeaderValue::from_static("X-Theme"));
let transformed_plan = plan(
StatusCode::OK,
&transformed_headers,
&s,
Duration::ZERO,
)
.expect("transformed plan");
let transformed_plan = plan(StatusCode::OK, &transformed_headers, &s, Duration::ZERO)
.expect("transformed plan");
let cache_plan = origin_plan
.without_vary("accept-encoding")
.restrict(transformed_plan);
@@ -1059,11 +1082,20 @@ mod tests {
let entry = store.entries.values_mut().next().expect("stored object");
entry.received = Instant::now() - Duration::from_secs(2);
}
assert!(cache.get(&initial).is_none(), "normal request must not receive stale data");
assert_eq!(cache.view().entries, 1, "freshness expiry must not physically evict the object");
assert!(
cache.get(&initial).is_none(),
"normal request must not receive stale data"
);
assert_eq!(
cache.view().entries,
1,
"freshness expiry must not physically evict the object"
);
let stale = ticket(&cache, &headers("max-stale"));
let hit = cache.get(&stale).expect("explicit max-stale may reuse retained stale data");
let hit = cache
.get(&stale)
.expect("explicit max-stale may reuse retained stale data");
assert!(hit.stale);
assert_eq!(
hit.response.into_body().collect().await.unwrap().to_bytes(),
@@ -1099,7 +1131,14 @@ mod tests {
validation.insert("etag", HeaderValue::from_static("\"origin\""));
let merged = candidate.merge_validation_headers(&validation);
let hit = cache
.publish_revalidated(t.clone(), &candidate, merged, &validation, &s, Duration::ZERO)
.publish_revalidated(
t.clone(),
&candidate,
merged,
&validation,
&s,
Duration::ZERO,
)
.expect("validated body can be delivered");
assert_eq!(
hit.response.into_body().collect().await.unwrap().to_bytes(),
@@ -1142,7 +1181,14 @@ mod tests {
validation.insert("etag", HeaderValue::from_static("\"origin\""));
let merged = candidate.merge_validation_headers(&validation);
let hit = cache
.publish_revalidated(t, &candidate, merged, &validation, &s, Duration::from_secs(2))
.publish_revalidated(
t,
&candidate,
merged,
&validation,
&s,
Duration::from_secs(2),
)
.expect("validated object");
assert!(hit.resident_age_seconds >= 32);
}
@@ -1284,7 +1330,10 @@ mod tests {
assert!(after.hot_allocated_bytes <= s.max_bytes);
assert_eq!(after.fill_bytes_active, 0);
assert_eq!(after.transient_bytes, 0);
assert!(cache.get(&first).is_none(), "LRU entry should make room before the next fill allocation");
assert!(
cache.get(&first).is_none(),
"LRU entry should make room before the next fill allocation"
);
assert!(cache.get(&second).is_some());
assert!(after.evictions >= 1);
}
@@ -1418,12 +1467,17 @@ mod tests {
assert_eq!(first, Bytes::from_static(b"first-"));
drop(body);
assert!(cache.get(&t).is_none(), "partial body must never become a HIT");
assert!(
cache.get(&t).is_none(),
"partial body must never become a HIT"
);
assert_eq!(cache.view().fills_active, 0);
assert_eq!(cache.view().rejected_fills, 1);
fill(&cache, t.clone(), "complete", &s).await;
let hit = cache.get(&t).expect("a later complete GET can fill the cache");
let hit = cache
.get(&t)
.expect("a later complete GET can fill the cache");
assert_eq!(
hit.response.into_body().collect().await.unwrap().to_bytes(),
Bytes::from_static(b"complete")
@@ -1528,18 +1582,30 @@ mod scope_tests {
assert!(effective.enabled && effective.expose_status);
let mut globally_disabled = global.clone();
globally_disabled.enabled = false;
assert!(!Overrides { enabled: Some(true), ..Overrides::default() }
assert!(
!Overrides {
enabled: Some(true),
..Overrides::default()
}
.resolve(&globally_disabled)
.unwrap()
.enabled);
assert!(!Overrides { enabled: Some(false), ..Overrides::default() }
.enabled
);
assert!(
!Overrides {
enabled: Some(false),
..Overrides::default()
}
.resolve(&global)
.unwrap()
.enabled);
assert!(!Overrides::default()
.resolve(&globally_disabled)
.unwrap()
.enabled);
.enabled
);
assert!(
!Overrides::default()
.resolve(&globally_disabled)
.unwrap()
.enabled
);
assert_eq!(effective.max_bytes, global.max_bytes);
assert_eq!(effective.max_ttl_seconds, 30);
assert!(Overrides {
@@ -1861,8 +1927,22 @@ mod v022_cache_tests {
)
.await;
let cases = [
(ByteRange { start: None, end: Some(3) }, "hij", "bytes 7-9/10"),
(ByteRange { start: Some(7), end: None }, "hij", "bytes 7-9/10"),
(
ByteRange {
start: None,
end: Some(3),
},
"hij",
"bytes 7-9/10",
),
(
ByteRange {
start: Some(7),
end: None,
},
"hij",
"bytes 7-9/10",
),
];
for (range, expected, content_range) in cases {
let hit = cache.get(&ticket).unwrap().apply_range(range, None);
@@ -1873,38 +1953,66 @@ mod v022_cache_tests {
}
let unsatisfied = cache.get(&ticket).unwrap().apply_range(
ByteRange { start: Some(50), end: None },
ByteRange {
start: Some(50),
end: None,
},
None,
);
assert_eq!(unsatisfied.response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
assert_eq!(unsatisfied.response.headers()["content-range"], "bytes */10");
assert_eq!(
unsatisfied.response.into_body().collect().await.unwrap().to_bytes().len(),
unsatisfied.response.status(),
StatusCode::RANGE_NOT_SATISFIABLE
);
assert_eq!(
unsatisfied.response.headers()["content-range"],
"bytes */10"
);
assert_eq!(
unsatisfied
.response
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.len(),
0
);
let mismatch = HeaderValue::from_static("\"asset-v2\"");
let full = cache.get(&ticket).unwrap().apply_range(
ByteRange { start: Some(0), end: Some(2) },
ByteRange {
start: Some(0),
end: Some(2),
},
Some(&mismatch),
);
assert_eq!(full.response.status(), StatusCode::OK);
assert!(!full.response.headers().contains_key("content-range"));
assert_eq!(
full.response.into_body().collect().await.unwrap().to_bytes(),
full.response
.into_body()
.collect()
.await
.unwrap()
.to_bytes(),
Bytes::from_static(b"abcdefghij")
);
let matched = HeaderValue::from_static("\"asset-v1\"");
let partial = cache.get(&ticket).unwrap().apply_range(
ByteRange { start: Some(0), end: Some(2) },
ByteRange {
start: Some(0),
end: Some(2),
},
Some(&matched),
);
assert_eq!(partial.response.status(), StatusCode::PARTIAL_CONTENT);
}
#[test]
fn range_parser_rejects_multipart_and_duplicate_fields_and_if_range_date_allows_older_representation() {
fn range_parser_rejects_multipart_and_duplicate_fields_and_if_range_date_allows_older_representation(
) {
let mut multipart = HeaderMap::new();
multipart.insert("range", HeaderValue::from_static("bytes=0-1,4-5"));
assert_eq!(
@@ -1999,13 +2107,23 @@ mod v022_cache_tests {
let mut settings = active_settings();
settings.collapse_wait_timeout_ms = 10;
let ticket = cache
.ticket(22, "http", "collapse.example", "origin", "/timeout", &HeaderMap::new())
.ticket(
22,
"http",
"collapse.example",
"origin",
"/timeout",
&HeaderMap::new(),
)
.unwrap();
let leader = match cache.claim_fill(&ticket, &settings).await {
FillClaim::Leader(guard) => guard,
_ => panic!("missing leader"),
};
assert!(matches!(cache.claim_fill(&ticket, &settings).await, FillClaim::Timeout));
assert!(matches!(
cache.claim_fill(&ticket, &settings).await,
FillClaim::Timeout
));
assert_eq!(cache.view().collapse_timeouts, 1);
drop(leader);
assert!(matches!(
@@ -2020,7 +2138,14 @@ mod v022_cache_tests {
let mut settings = active_settings();
settings.collapse_wait_timeout_ms = 5_000;
let ticket = cache
.ticket(22, "http", "collapse.example", "origin", "/cancel", &HeaderMap::new())
.ticket(
22,
"http",
"collapse.example",
"origin",
"/cancel",
&HeaderMap::new(),
)
.unwrap();
let leader = match cache.claim_fill(&ticket, &settings).await {
FillClaim::Leader(guard) => guard,
@@ -2058,10 +2183,24 @@ mod v022_cache_tests {
let mut pl_headers = HeaderMap::new();
pl_headers.insert("accept-language", HeaderValue::from_static("pl"));
let en = cache
.ticket(22, "http", "vary-collapse.example", "origin", "/page", &en_headers)
.ticket(
22,
"http",
"vary-collapse.example",
"origin",
"/page",
&en_headers,
)
.unwrap();
let pl = cache
.ticket(22, "http", "vary-collapse.example", "origin", "/page", &pl_headers)
.ticket(
22,
"http",
"vary-collapse.example",
"origin",
"/page",
&pl_headers,
)
.unwrap();
assert_eq!(en.key, pl.key, "Vary is learned from the response");
let leader = match cache.claim_fill(&en, &settings).await {
@@ -2136,7 +2275,14 @@ mod v022_cache_tests {
fn proactive_refresh_guards_deduplicate_and_bound_parallel_work() {
let cache = Cache::new(22);
let ticket = cache
.ticket(22, "http", "refresh.example", "origin", "/asset", &HeaderMap::new())
.ticket(
22,
"http",
"refresh.example",
"origin",
"/asset",
&HeaderMap::new(),
)
.unwrap();
let mut settings = active_settings();
settings.proactive_refresh_enabled = true;
@@ -2192,7 +2338,9 @@ mod v022_cache_tests {
&HeaderMap::new(),
)
.unwrap();
assert!(cache.begin_proactive_schedule(&overflow, &settings).is_none());
assert!(cache
.begin_proactive_schedule(&overflow, &settings)
.is_none());
assert!(cache.view().proactive_skipped > 0);
drop(guards);
}
@@ -2502,7 +2650,10 @@ mod v022_cache_tests {
"fresh",
)
.await;
assert!(cache.get(&fresh).is_some(), "old BAN must not reject newer objects");
assert!(
cache.get(&fresh).is_some(),
"old BAN must not reject newer objects"
);
assert_eq!(cache.bans().len(), 1);
assert!(cache.remove_ban(ban_id));
assert!(cache.bans().is_empty());
@@ -2729,9 +2880,10 @@ mod cold_tier_tests {
"public, max-age=120, stale-while-revalidate=30, stale-if-error=90",
),
);
transformed
.headers_mut()
.insert("etag", HeaderValue::from_static("\"rustproxy-representation\""));
transformed.headers_mut().insert(
"etag",
HeaderValue::from_static("\"rustproxy-representation\""),
);
transformed
.headers_mut()
.insert("content-length", HeaderValue::from_static("7"));
@@ -2739,15 +2891,15 @@ mod cold_tier_tests {
.extensions_mut()
.insert(crate::transform::OriginValidators {
etag: Some(HeaderValue::from_static("\"origin-v1\"")),
last_modified: Some(HeaderValue::from_static(
"Mon, 26 Jan 2026 23:11:40 GMT",
)),
last_modified: Some(HeaderValue::from_static("Mon, 26 Jan 2026 23:11:40 GMT")),
});
transformed
.extensions_mut()
.insert(crate::transform::Applied {
kind: crate::transform::Kind::Json,
original_bytes: 9,
transformed_bytes: 7,
});
transformed.extensions_mut().insert(crate::transform::Applied {
kind: crate::transform::Kind::Json,
original_bytes: 9,
transformed_bytes: 7,
});
let transformed_plan = plan(
StatusCode::OK,
transformed.headers(),
@@ -2868,7 +3020,9 @@ mod cold_tier_tests {
.await
.expect("no-transform object after restart");
assert_eq!(protected_hit.response.headers()["etag"], "\"origin-css\"");
assert!(crate::transform::no_transform(protected_hit.response.headers()));
assert!(crate::transform::no_transform(
protected_hit.response.headers()
));
assert!(protected_hit
.response
.extensions()
@@ -2994,7 +3148,10 @@ mod cold_tier_tests {
assert_eq!(view.entries, 0);
assert_eq!(view.resident_bytes, 0);
assert_eq!(view.hot_logical_bytes, 0);
assert_eq!(view.transient_bytes, 0, "persisted ColdEntry must not retain the body allocation");
assert_eq!(
view.transient_bytes, 0,
"persisted ColdEntry must not retain the body allocation"
);
assert_eq!(view.cold_entries, 2);
shutdown.cancel();
@@ -3072,17 +3229,23 @@ mod cold_tier_tests {
for item in std::fs::read_dir(&root).unwrap() {
let item = item.unwrap();
let name = item.file_name().to_string_lossy().to_string();
assert!(item.path().is_dir(), "cold root must contain shard directories only");
assert!(
item.path().is_dir(),
"cold root must contain shard directories only"
);
assert_eq!(name.len(), 2);
assert!(name.bytes().all(|b| b.is_ascii_hexdigit()));
shards.push(item.path());
}
assert!(!shards.is_empty());
assert!(shards.iter().any(|shard| {
std::fs::read_dir(shard).unwrap().filter_map(|entry| entry.ok()).any(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
name.ends_with(".json") || name.ends_with(".body")
})
std::fs::read_dir(shard)
.unwrap()
.filter_map(|entry| entry.ok())
.any(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
name.ends_with(".json") || name.ends_with(".body")
})
}));
shutdown.cancel();
}
@@ -3096,7 +3259,12 @@ mod cold_tier_tests {
std::fs::write(root.join("legacy.body"), b"old").unwrap();
std::fs::write(root.join("legacy.json"), b"{}").unwrap();
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings,
shutdown.clone(),
);
assert_eq!(cache.view().cold_entries, 0);
assert!(!root.join("legacy.body").exists());
assert!(!root.join("legacy.json").exists());
@@ -3109,12 +3277,23 @@ mod cold_tier_tests {
let mut settings = cold_settings();
settings.max_entries = 1;
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings.clone(), shutdown.clone());
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
fill(&cache, &settings, "/three", "third-payload").await;
wait_for_cold(&cache, 2).await;
let job = cache.start_cold_cleanup(ColdCleanupRequest { unused_for_seconds: None, oldest: Some(1), hits_zero: false }).unwrap();
let job = cache
.start_cold_cleanup(ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
})
.unwrap();
for _ in 0..100 {
let current = cache.cold_cleanup_job(job.id).unwrap();
if current.status == "done" {
@@ -3134,7 +3313,12 @@ mod cold_tier_tests {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings,
shutdown.clone(),
);
let _held = cache.cleanup_slots.clone().try_acquire_owned().unwrap();
let result = cache.start_cold_cleanup(ColdCleanupRequest {
unused_for_seconds: None,
@@ -3178,7 +3362,14 @@ mod cold_tier_tests {
assert_eq!(result.removed_entries, 0);
assert!(cache.cold.lock().unwrap().entries.contains_key(&key));
cache.cold.lock().unwrap().entries.get_mut(&key).unwrap().pinned_reads = 0;
cache
.cold
.lock()
.unwrap()
.entries
.get_mut(&key)
.unwrap()
.pinned_reads = 0;
let result = cache
.remove_cold_candidates(Some(&request), None, None, 1)
.await;