0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
Vendored
+30
-15
@@ -1,20 +1,22 @@
|
||||
impl Cache {
|
||||
fn get_hot(&self, ticket: &Ticket, count_miss: bool) -> Option<Hit> {
|
||||
// Only metadata, hit counters and exact LRU ordering require the store
|
||||
// mutex. Materializing an HTTP response may allocate (headers, Body,
|
||||
// Age formatting) and must be done *after* dropping that mutex.
|
||||
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if store.revision != ticket.revision
|
||||
|| store.generation != ticket.generation
|
||||
|| store.scope_generations.get(&ticket.scope).copied().unwrap_or(0)
|
||||
|| store
|
||||
.scope_generations
|
||||
.get(&ticket.scope)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
!= ticket.scope_generation
|
||||
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0)
|
||||
!= ticket.key_generation
|
||||
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0) != ticket.key_generation
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(storage_key) = matching_hot_key(&store, ticket) {
|
||||
let banned = store.entries.get(&storage_key)
|
||||
let banned = store
|
||||
.entries
|
||||
.get(&storage_key)
|
||||
.is_some_and(|entry| self.hot_entry_banned(&storage_key, entry));
|
||||
if banned {
|
||||
store.remove(&storage_key);
|
||||
@@ -27,7 +29,10 @@ impl Cache {
|
||||
}
|
||||
let entry = store.entries.get(&storage_key)?;
|
||||
let resident_age_seconds = hot_resident_age(entry).as_secs();
|
||||
let age = entry.initial_age.saturating_add(entry.received.elapsed()).as_secs();
|
||||
let age = entry
|
||||
.initial_age
|
||||
.saturating_add(entry.received.elapsed())
|
||||
.as_secs();
|
||||
let state = cache_state(
|
||||
age,
|
||||
entry.ttl,
|
||||
@@ -39,8 +44,16 @@ impl Cache {
|
||||
);
|
||||
if state.servable() {
|
||||
let sequence = store.next_sequence();
|
||||
let (old_sequence, headers, body, hits, transformed, skip_compression,
|
||||
remaining_ttl, body_bytes) = {
|
||||
let (
|
||||
old_sequence,
|
||||
headers,
|
||||
body,
|
||||
hits,
|
||||
transformed,
|
||||
skip_compression,
|
||||
remaining_ttl,
|
||||
body_bytes,
|
||||
) = {
|
||||
let entry = store.entries.get_mut(&storage_key)?;
|
||||
let old_sequence = entry.sequence;
|
||||
entry.hits = entry.hits.saturating_add(1);
|
||||
@@ -59,15 +72,15 @@ impl Cache {
|
||||
};
|
||||
store.lru.remove(&old_sequence);
|
||||
store.lru.insert(sequence, storage_key);
|
||||
// Preserve the exact LRU and domain-byte counters, without
|
||||
// allocating a replacement variant index or String keys.
|
||||
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||
drop(store);
|
||||
|
||||
let mut response = Response::new(Body::from(body.clone()));
|
||||
*response.headers_mut() = headers.as_ref().clone();
|
||||
if transformed {
|
||||
response.extensions_mut().insert(crate::transform::CachedTransformed);
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(crate::transform::CachedTransformed);
|
||||
}
|
||||
if skip_compression {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
@@ -76,7 +89,8 @@ impl Cache {
|
||||
"age",
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
|
||||
self.domain_stats
|
||||
.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
|
||||
return Some(Hit {
|
||||
response,
|
||||
body,
|
||||
@@ -274,7 +288,8 @@ impl Cache {
|
||||
"age",
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, entry.body_bytes);
|
||||
self.domain_stats
|
||||
.saved_body_hit(&ticket.scope, &ticket.domain, entry.body_bytes);
|
||||
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||
self.counters.cold_hits.fetch_add(1, Ordering::Relaxed);
|
||||
self.counters
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
mod hot_hit_034_tests {
|
||||
use super::*;
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
enabled: true,
|
||||
..Settings::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish(
|
||||
cache: &Arc<Cache>,
|
||||
settings: &Settings,
|
||||
path: &str,
|
||||
body: &'static str,
|
||||
transformed: bool,
|
||||
) -> Ticket {
|
||||
let ticket = cache
|
||||
.ticket_scoped(
|
||||
34,
|
||||
"app",
|
||||
"http",
|
||||
"cache.example",
|
||||
"origin@127.0.0.1:8080",
|
||||
path,
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.expect("cache ticket");
|
||||
let mut response = Response::new(Body::from(body));
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=31536000"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"etag",
|
||||
HeaderValue::from_static("\"cached-representation\""),
|
||||
);
|
||||
if transformed {
|
||||
response.extensions_mut().insert(crate::transform::Applied {
|
||||
kind: crate::transform::Kind::Css,
|
||||
original_bytes: body.len(),
|
||||
transformed_bytes: body.len(),
|
||||
});
|
||||
}
|
||||
let policy = plan(StatusCode::OK, response.headers(), settings, Duration::ZERO)
|
||||
.expect("public static object cacheable");
|
||||
cache
|
||||
.wrap(response, ticket.clone(), policy, settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
ticket
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hot_hit_headers_are_independent_and_lru_counters_remain_exact() {
|
||||
let cache = Cache::new(34);
|
||||
let settings = settings();
|
||||
let ticket = publish(&cache, &settings, "/theme.css", "body{color:red}", true).await;
|
||||
let initial = cache.view();
|
||||
let before = cache.dashboard(Some("app"), 0, 10);
|
||||
assert_eq!(before.matching_objects, 1);
|
||||
assert!(before.objects[0].transformed);
|
||||
|
||||
for i in 1..=500 {
|
||||
let hit = cache.get(&ticket).expect("HOT cache hit");
|
||||
assert_eq!(hit.hits, i);
|
||||
assert!(hit.transformed);
|
||||
assert!(hit
|
||||
.response
|
||||
.extensions()
|
||||
.get::<crate::transform::CachedTransformed>()
|
||||
.is_some());
|
||||
assert_eq!(hit.response.headers()["etag"], "\"cached-representation\"");
|
||||
let mut independent = hit.response;
|
||||
independent
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"client-only\""));
|
||||
if i == 500 {
|
||||
assert_eq!(
|
||||
independent.into_body().collect().await.unwrap().to_bytes(),
|
||||
Bytes::from_static(b"body{color:red}")
|
||||
);
|
||||
}
|
||||
}
|
||||
let after = cache.view();
|
||||
assert_eq!(after.entries, 1);
|
||||
assert_eq!(after.hot_allocated_bytes, initial.hot_allocated_bytes);
|
||||
assert_eq!(after.hot_logical_bytes, initial.hot_logical_bytes);
|
||||
assert_eq!(after.hits.saturating_sub(initial.hits), 500);
|
||||
let store = cache.store.lock().unwrap();
|
||||
assert_eq!(store.entries.len(), 1);
|
||||
assert_eq!(store.lru.len(), 1);
|
||||
assert_eq!(store.variants.len(), 1);
|
||||
drop(store);
|
||||
let dashboard = cache.dashboard(Some("app"), 0, 10);
|
||||
assert_eq!(dashboard.objects[0].hits, 500);
|
||||
assert_eq!(dashboard.domains[0].counters.saved_body_bytes, 500 * 15);
|
||||
assert_eq!(
|
||||
cache.get(&ticket).unwrap().response.headers()["etag"],
|
||||
"\"cached-representation\""
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_hot_hits_have_unique_monotonic_counts_and_bounded_indexes() {
|
||||
let cache = Cache::new(34);
|
||||
let ticket = publish(&cache, &settings(), "/shared.css", "cachebody", false).await;
|
||||
let workers = 6;
|
||||
let per_worker = 300;
|
||||
let mut threads = Vec::new();
|
||||
for _ in 0..workers {
|
||||
let cache = Arc::clone(&cache);
|
||||
let ticket = ticket.clone();
|
||||
threads.push(std::thread::spawn(move || {
|
||||
let mut hits = Vec::with_capacity(per_worker);
|
||||
for _ in 0..per_worker {
|
||||
let hit = cache.get(&ticket).expect("concurrent HOT hit");
|
||||
assert_eq!(hit.response.headers()["etag"], "\"cached-representation\"");
|
||||
hits.push(hit.hits);
|
||||
}
|
||||
hits
|
||||
}));
|
||||
}
|
||||
let mut counts: Vec<_> = threads
|
||||
.into_iter()
|
||||
.flat_map(|t| t.join().unwrap())
|
||||
.collect();
|
||||
counts.sort_unstable();
|
||||
assert_eq!(
|
||||
counts,
|
||||
(1..=(workers * per_worker) as u64).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(cache.view().hits, (workers * per_worker) as u64);
|
||||
let store = cache.store.lock().unwrap();
|
||||
assert_eq!(store.lru.len(), 1);
|
||||
assert_eq!(store.entries.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hot_lru_eviction_and_ban_are_preserved() {
|
||||
let cache = Cache::new(34);
|
||||
let settings = Settings {
|
||||
max_entries: 2,
|
||||
..settings()
|
||||
};
|
||||
let a = publish(&cache, &settings, "/a.css", "AAAA", false).await;
|
||||
let b = publish(&cache, &settings, "/b.css", "BBBB", false).await;
|
||||
assert!(cache.get(&a).is_some(), "A becomes most recently used");
|
||||
let c = publish(&cache, &settings, "/c.css", "CCCC", false).await;
|
||||
assert!(cache.get(&a).is_some());
|
||||
assert!(cache.get(&b).is_none(), "B is least recently used");
|
||||
assert!(cache.get(&c).is_some());
|
||||
|
||||
let ban = cache
|
||||
.invalidate(InvalidationRequest {
|
||||
operation: InvalidationOperation::Ban,
|
||||
selector: InvalidationSelector::Exact,
|
||||
value: "/a.css".into(),
|
||||
host: Some("cache.example".into()),
|
||||
vhost: Some("app".into()),
|
||||
ttl_seconds: Some(3600),
|
||||
confirm_wide: false,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 1);
|
||||
assert!(cache.get(&a).is_none(), "BAN must block cached A");
|
||||
assert!(cache.remove_ban(ban.ban_id.unwrap()));
|
||||
assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 0);
|
||||
assert!(cache.get(&c).is_some(), "unrelated C must remain cached");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "manual in-process timing only; NOT HTTP RPS"]
|
||||
async fn manual_hot_lookup_microbenchmark() {
|
||||
let cache = Cache::new(34);
|
||||
let ticket = publish(&cache, &settings(), "/bench.css", "smallbody", true).await;
|
||||
for _ in 0..1_000 {
|
||||
std::hint::black_box(cache.get(&ticket).unwrap());
|
||||
}
|
||||
let iterations = 100_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
std::hint::black_box(cache.get(&ticket).unwrap());
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
eprintln!(
|
||||
"HOT lookup incl. response materialization: {:.1} ns/op ({} hits); NOT network RPS",
|
||||
duration.as_secs_f64() * 1e9 / iterations as f64,
|
||||
iterations
|
||||
);
|
||||
assert_eq!(cache.view().hits, iterations + 1_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_object_remains_available_for_explicit_max_stale() {
|
||||
let cache = Cache::new(34);
|
||||
let ticket = publish(&cache, &settings(), "/old.css", "oldbody", false).await;
|
||||
{
|
||||
let mut store = cache.store.lock().unwrap();
|
||||
store.entries.values_mut().next().unwrap().received =
|
||||
Instant::now() - Duration::from_secs(700_000);
|
||||
}
|
||||
assert!(
|
||||
cache.get(&ticket).is_none(),
|
||||
"expired freshness is not an unconditional HIT"
|
||||
);
|
||||
assert_eq!(
|
||||
cache.view().entries,
|
||||
1,
|
||||
"stale representation must survive for revalidation"
|
||||
);
|
||||
let mut stale_ticket = ticket;
|
||||
stale_ticket
|
||||
.request_headers
|
||||
.insert("cache-control", HeaderValue::from_static("max-stale"));
|
||||
assert!(cache.get(&stale_ticket).unwrap().stale);
|
||||
}
|
||||
}
|
||||
Vendored
-3
@@ -44,9 +44,6 @@ impl Cache {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Do not keep objects for incompatible Vary schemas under the same
|
||||
// Varnish-style host+URL hash. A newer representation policy replaces
|
||||
// the older schema while preserving sibling values of the same Vary set.
|
||||
for key in incompatible_hot_variants(&store, &ticket.key, &vary_schema) {
|
||||
store.remove(&key);
|
||||
}
|
||||
|
||||
Vendored
+9
-7
@@ -170,10 +170,15 @@ impl Cache {
|
||||
Some(validators) => validators.last_modified.clone(),
|
||||
None => headers.get("last-modified").cloned(),
|
||||
};
|
||||
let transformed = parts.extensions.get::<crate::transform::Applied>().is_some();
|
||||
// A transformed, fully buffered body normally arrives as one owned
|
||||
// frame. Do not allocate a second fill buffer before inspecting it.
|
||||
let initial_capacity = if transformed { 0 } else { fill_size.min(64 * 1024) };
|
||||
let transformed = parts
|
||||
.extensions
|
||||
.get::<crate::transform::Applied>()
|
||||
.is_some();
|
||||
let initial_capacity = if transformed {
|
||||
0
|
||||
} else {
|
||||
fill_size.min(64 * 1024)
|
||||
};
|
||||
let Some(mut fill_gauge) = FillGauge::new(self.clone(), initial_capacity, settings) else {
|
||||
self.counters.rejected_fills.fetch_add(1, Ordering::Relaxed);
|
||||
self.record_reason(&ticket.scope, &ticket.domain, "hot_memory_pressure");
|
||||
@@ -205,9 +210,6 @@ 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);
|
||||
|
||||
Reference in new issue
Block a user