v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
Vendored
+84
-34
@@ -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) {
|
||||
|
||||
Vendored
+10
-28
@@ -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
|
||||
|
||||
Vendored
+21
-5
@@ -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 {
|
||||
|
||||
Vendored
+2
-2
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+80
-18
@@ -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 {
|
||||
|
||||
Vendored
+23
-16
@@ -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);
|
||||
}
|
||||
|
||||
Vendored
+42
-16
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+315
-124
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user