v0.33.8
This commit is contained in:
1 parent
6b937aaf85
commit
87c8393d86
79 files changed
+6727
-297
No files matched your search
Vendored
+3
-4
@@ -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 {
|
||||
|
||||
Vendored
+7
@@ -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 {
|
||||
|
||||
Vendored
+31
@@ -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();
|
||||
|
||||
Vendored
+9
-2
@@ -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() {
|
||||
|
||||
Vendored
+74
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+17
-6
@@ -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(¤t_ticket(&cache, &one)).is_none());
|
||||
let survivor = cache.get(¤t_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(¤t_ticket(&cache, &three)).is_none());
|
||||
assert!(cache.get(¤t_ticket(&cache, &four)).is_none());
|
||||
assert!(cache.get(¤t_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(¤t_ticket(&cache, &tagged)).is_none());
|
||||
assert!(cache.get(¤t_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(¤t_ticket(&cache, &keyed)).is_none());
|
||||
assert!(cache.get(¤t_ticket(&cache, &two)).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user