This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 11:57:16 +02:00
1 parent 6b937aaf85
commit 87c8393d86
79 files changed
+6727 -297

No files matched your search

+3 -4
View File
@@ -323,7 +323,9 @@ impl Cache {
continue;
};
let age = e.initial_age.saturating_add(e.received.elapsed()).as_secs();
if age >= e.ttl || !scope.is_none_or(|s| s == e.scope) {
// This is an inventory of retained objects, not a freshness lookup.
// Expired representations still occupy RAM and may be revalidated.
if !scope.is_none_or(|s| s == e.scope) {
continue;
}
let row = groups
@@ -373,9 +375,6 @@ impl Cache {
continue;
}
let age = cold_age(e);
if age >= e.ttl && !e.requires_revalidation {
continue;
}
let row = groups
.entry((e.scope.clone(), e.domain.clone()))
.or_insert_with(|| DomainView {
+7
View File
@@ -205,6 +205,13 @@ impl Cache {
_permit: permit,
_fill_leader: fill_leader,
};
// A genuinely finished body (including an empty local file) may never
// be polled by Hyper. EOS is the proof here, not Content-Length alone;
// finish still enforces expected length, privacy and ticket validity.
if body.is_end_stream() {
candidate.finish();
return Response::from_parts(parts, body);
}
Response::from_parts(
parts,
Body::new(FillBody {
+31
View File
@@ -134,6 +134,37 @@ async fn lru_demotion_persists_and_survives_restart() {
restart_shutdown.cancel();
}
#[tokio::test]
async fn dashboard_lists_retained_expired_cold_objects_without_permitting_fresh_hits() {
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.clone(), shutdown.clone());
let first = fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
wait_for_cold(&cache, 1).await;
{
let mut cold = cache.cold.lock().unwrap();
for entry in cold.entries.values_mut() {
entry.stored_at = SystemTime::now() - Duration::from_secs(2 * 60 * 60);
}
}
let dashboard = cache.dashboard(Some("cold-test"), 0, 50);
assert_eq!(dashboard.matching_objects, 2);
let row = dashboard.objects.iter().find(|row| row.path == "/one").unwrap();
assert_eq!(row.tier, "cold");
assert_eq!(row.remaining_ttl_seconds, 0);
assert!(row.age_seconds >= 2 * 60 * 60);
assert_eq!(row.storage_bytes, cache.view().cold_bytes);
assert!(cache.get_tiered(&first, &settings).await.is_none());
let mut stale = first;
stale.request_headers.insert("cache-control", HeaderValue::from_static("max-stale"));
let hit = cache.get_tiered(&stale, &settings).await.expect("explicit max-stale can reuse the retained disk object");
assert!(hit.stale);
assert_eq!(hit.response.into_body().collect().await.unwrap().to_bytes(), "first-payload");
shutdown.cancel();
}
#[tokio::test]
async fn cold_restart_preserves_transformed_validators_stale_metadata_and_no_transform() {
let dir = tempfile::tempdir().unwrap();
+9 -2
View File
@@ -78,7 +78,7 @@ mod dashboard_tests {
#[tokio::test]
async fn dashboard_retains_expired_entries_for_client_max_stale() {
let cache = Cache::new(7);
fill(&cache, "a", "a.example", "/").await;
let ticket = fill(&cache, "a", "a.example", "/").await;
{
let mut store = cache.store.lock().unwrap();
for entry in store.entries.values_mut() {
@@ -86,9 +86,16 @@ mod dashboard_tests {
}
store.last_sweep = None;
}
assert_eq!(cache.dashboard(None, 0, 50).matching_objects, 1);
let dashboard = cache.dashboard(None, 0, 50);
assert_eq!(dashboard.matching_objects, 1);
assert_eq!(dashboard.objects[0].remaining_ttl_seconds, 0);
assert!(dashboard.objects[0].age_seconds >= 2 * 60 * 60);
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.view().expired, 0);
assert!(cache.get(&ticket).is_none(), "inventory visibility cannot permit unconditional stale delivery");
let mut stale = ticket;
stale.request_headers.insert("cache-control", HeaderValue::from_static("max-stale"));
assert!(cache.get(&stale).unwrap().stale);
}
#[test]
fn bypass_reasons_are_fixed_and_private_inputs_are_not_echoed() {
+74 -13
View File
@@ -23,10 +23,8 @@ mod tests {
for control in [
"private, max-age=60",
"no-store, max-age=60",
"max-age=0",
"max-age=invalid",
"max-age=60,max-age=90",
"public",
"must-understand, max-age=60",
"max-age=\"60",
"max-age=60\"",
@@ -53,13 +51,6 @@ mod tests {
let mut validator_only = headers("no-cache");
validator_only.insert("etag", HeaderValue::from_static("\"validator\""));
assert!(plan(StatusCode::OK, &validator_only, &s, Duration::ZERO).is_some());
assert!(plan(
StatusCode::OK,
&headers("max-age=60,s-maxage=0"),
&s,
Duration::ZERO
)
.is_none());
let shared = plan(
StatusCode::OK,
&headers("public, max-age=600, s-maxage=30"),
@@ -75,6 +66,24 @@ mod tests {
h.insert("set-cookie", "session=secret".parse().unwrap());
assert!(plan(StatusCode::OK, &h, &s, Duration::ZERO).is_none());
}
#[tokio::test]
async fn zero_freshness_is_retained_but_never_served_as_an_unconditional_hit() {
for control in ["max-age=0", "public", "max-age=60,s-maxage=0"] {
let cache = Cache::new(1);
let settings = settings();
let ticket = ticket(&cache, &HeaderMap::new());
let mut response = Response::new(Body::from("zero-freshness"));
*response.headers_mut() = headers(control);
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO)
.expect("valid zero freshness is distinct from a no-store response");
assert_eq!(plan.ttl, 0, "{control}");
cache.wrap(response, ticket.clone(), plan, &settings)
.into_body().collect().await.unwrap();
assert_eq!(cache.view().entries, 1, "{control}");
assert!(cache.get(&ticket).is_none(), "zero-TTL {control} cannot be an unconditional HIT");
}
}
#[test]
fn request_policy_uses_conditional_cache_and_revalidation() {
for name in [
@@ -1362,21 +1371,23 @@ mod tests {
&a,
)
.unwrap();
let other_transport = cache
// Compare unrelated headers/member choice within the same frontend
// scheme. HTTP and HTTPS intentionally have distinct cache identities.
let other_member = cache
.ticket(
1,
"https",
"http",
"example.test",
"pool/member-b",
"/asset.js?x=1",
&b,
)
.unwrap();
assert_eq!(base.key, other_transport.key);
assert_eq!(base.key, other_member.key);
let other_url = cache
.ticket(
1,
"https",
"http",
"example.test",
"pool/member-b",
"/asset.js?x=2",
@@ -1479,4 +1490,54 @@ mod tests {
assert_eq!(cache.view().fills_active, 0);
}
}
#[test]
fn empty_complete_body_is_cached_without_requiring_a_consumer_poll() {
let cache = Cache::new(1);
let settings = settings();
let ticket = ticket(&cache, &HeaderMap::new());
let mut response = Response::new(Body::empty());
*response.headers_mut() = headers("public, max-age=600");
response.headers_mut().insert("content-length", HeaderValue::from_static("0"));
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
let response = cache.wrap(response, ticket.clone(), plan, &settings);
assert!(response.body().is_end_stream());
// HTTP/1.1 Content-Length: 0 does not ask the response body for frames.
drop(response);
assert!(cache.get(&ticket).is_some());
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.view().fills_active, 0);
assert_eq!(cache.view().rejected_fills, 0);
}
#[test]
fn empty_body_with_nonzero_content_length_cannot_publish_a_fill() {
let cache = Cache::new(1);
let settings = settings();
let ticket = ticket(&cache, &HeaderMap::new());
let mut response = Response::new(Body::empty());
*response.headers_mut() = headers("public, max-age=600");
response.headers_mut().insert("content-length", HeaderValue::from_static("6"));
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
drop(cache.wrap(response, ticket.clone(), plan, &settings));
assert!(cache.get(&ticket).is_none());
assert_eq!(cache.view().fills_active, 0);
assert_eq!(cache.view().rejected_fills, 1);
}
#[test]
fn empty_body_completion_still_obeys_purge_barrier() {
let cache = Cache::new(1);
let settings = settings();
let ticket = ticket(&cache, &HeaderMap::new());
let mut response = Response::new(Body::empty());
*response.headers_mut() = headers("public, max-age=600");
response.headers_mut().insert("content-length", HeaderValue::from_static("0"));
let plan = plan(StatusCode::OK, response.headers(), &settings, Duration::ZERO).unwrap();
cache.purge(None);
drop(cache.wrap(response, ticket.clone(), plan, &settings));
assert!(cache.get(&ticket).is_none());
assert_eq!(cache.view().entries, 0);
assert_eq!(cache.view().fills_active, 0);
}
}
+17 -6
View File
@@ -556,6 +556,13 @@ mod v022_cache_tests {
#[tokio::test]
async fn selective_purge_exact_prefix_tag_cache_key_and_query_are_precise() {
fn current_ticket(cache: &Cache, old: &Ticket) -> Ticket {
// Each post-PURGE request obtains the current publication generation.
// Reusing an old ticket would reject unrelated entries too and make
// assertions about removed objects pass without testing the selector.
cache.ticket_scoped(22, &old.scope, &old.scheme, &old.authority,
"origin@127.0.0.1:8080", &old.target, &old.request_headers).unwrap()
}
let cache = Cache::new(22);
let one = store(
&cache,
@@ -587,8 +594,9 @@ mod v022_cache_tests {
))
.unwrap();
assert_eq!(result.affected_entries, 1);
assert!(cache.get(&one).is_none());
assert!(cache.get(&two).is_some());
assert!(cache.get(&current_ticket(&cache, &one)).is_none());
let survivor = cache.get(&current_ticket(&cache, &two)).expect("unrelated query remains stored");
assert_eq!(survivor.response.into_body().collect().await.unwrap().to_bytes(), "two");
let three = store(
&cache,
@@ -623,8 +631,9 @@ mod v022_cache_tests {
.affected_entries,
2
);
assert!(cache.get(&three).is_none());
assert!(cache.get(&four).is_none());
assert!(cache.get(&current_ticket(&cache, &three)).is_none());
assert!(cache.get(&current_ticket(&cache, &four)).is_none());
assert!(cache.get(&current_ticket(&cache, &two)).is_some());
let tagged = store(
&cache,
@@ -649,7 +658,8 @@ mod v022_cache_tests {
.affected_entries,
1
);
assert!(cache.get(&tagged).is_none());
assert!(cache.get(&current_ticket(&cache, &tagged)).is_none());
assert!(cache.get(&current_ticket(&cache, &two)).is_some());
let keyed = store(
&cache,
@@ -675,7 +685,8 @@ mod v022_cache_tests {
.affected_entries,
1
);
assert!(cache.get(&keyed).is_none());
assert!(cache.get(&current_ticket(&cache, &keyed)).is_none());
assert!(cache.get(&current_ticket(&cache, &two)).is_some());
}
#[tokio::test]