This commit is contained in:
Mateusz Gruszczyński committed 2026-11-07 10:23:28 +01:00
1 parent ff42a13dbf
commit e7b4991316
14 files changed
+176 -50

No files matched your search

+39 -2
View File
@@ -6,7 +6,6 @@ mod tests {
h.insert("cache-control", value.parse().unwrap());
h
}
// Legacy policy; varnish-compatible behavior is tested separately.
fn settings() -> Settings {
Settings {
enabled: true,
@@ -1559,7 +1558,10 @@ mod tests {
let second = cache
.ticket(1, "http", "example.test", "app", "/second", &headers)
.unwrap();
fill(&cache, first.clone(), "first", &s).await;
// Stored bodies are trimmed to their real size, so the first object must
// itself be large for the next fill reservation to need its space.
let large: &'static str = Box::leak("f".repeat(60 * 1024).into_boxed_str());
fill(&cache, first.clone(), large, &s).await;
assert!(cache.get(&first).is_some());
let before = cache.view();
assert!(before.hot_allocated_bytes <= s.max_bytes);
@@ -1821,4 +1823,39 @@ mod tests {
assert_eq!(cache.view().entries, 0);
assert_eq!(cache.view().fills_active, 0);
}
#[tokio::test]
async fn stored_entries_do_not_pin_origin_buffers_or_spare_capacity() {
let cache = Cache::new(1);
let s = settings();
let t = ticket(&cache, &HeaderMap::new());
// Header values parsed by hyper are slices of a larger read buffer.
let read_buffer = Bytes::from(vec![b'x'; 64 * 1024]);
let mut h = headers("max-age=60");
h.insert(
"x-origin",
HeaderValue::from_maybe_shared(read_buffer.slice(0..16)).unwrap(),
);
h.insert(
::http::header::ETAG,
HeaderValue::from_maybe_shared(read_buffer.slice(16..24)).unwrap(),
);
let p = plan(StatusCode::OK, &h, &s, Duration::ZERO).unwrap();
let mut response = Response::new(Body::from("small body"));
*response.headers_mut() = h;
cache
.wrap(response, t.clone(), p, &s)
.into_body()
.collect()
.await
.unwrap();
assert!(cache.get(&t).is_some());
assert!(
read_buffer.try_into_mut().is_ok(),
"cached headers must not keep the origin read buffer alive"
);
let store = cache.store.lock().unwrap();
let entry = store.entries.values().next().expect("stored object");
assert_eq!(entry.body.len(), 10);
assert!(entry.body_allocated <= 10 + 10 / 8 + 1);
}
}
+40 -2
View File
@@ -1,3 +1,16 @@
fn detached_header_value(value: &HeaderValue) -> HeaderValue {
let mut detached = HeaderValue::from_bytes(value.as_bytes())
.expect("existing HTTP header value must remain valid when copied");
detached.set_sensitive(value.is_sensitive());
detached
}
fn detach_header_map(headers: &mut HeaderMap) {
for value in headers.values_mut() {
*value = detached_header_value(value);
}
}
struct ByteGaugeGuard<'a> {
counter: &'a AtomicU64,
bytes: u64,
@@ -102,6 +115,9 @@ impl Candidate {
let (body, body_allocation) = if let Some(body) = self.shared.take() {
(body, self.shared_allocated)
} else {
if self.data.capacity() > self.data.len().saturating_add(self.data.len() / 8) {
self.data.shrink_to_fit();
}
let allocated = self.data.capacity().max(self.data.len());
(Bytes::from(std::mem::take(&mut self.data)), allocated)
};
@@ -111,6 +127,12 @@ impl Candidate {
let domain = self.ticket.domain.clone();
let path = self.ticket.path.clone();
let target = self.ticket.target.clone();
detach_header_map(&mut self.headers);
self.origin_etag = self.origin_etag.as_ref().map(detached_header_value);
self.origin_last_modified = self
.origin_last_modified
.as_ref()
.map(detached_header_value);
let tags = cache_tags(&self.headers);
let cost = hot_entry_cost(
body_allocation,
@@ -226,7 +248,21 @@ impl HttpBody for FillBody {
let needed = candidate.data.len().saturating_add(data.len());
if needed <= candidate.settings.max_object_bytes {
let before = candidate.data.capacity();
let required_growth = needed.saturating_sub(before);
let target_capacity = if needed <= before {
before
} else if let Some(length) =
candidate.plan.expected_length.filter(|length| {
*length >= needed
&& *length <= candidate.settings.max_object_bytes
})
{
length
} else {
needed
.max(before.saturating_mul(2))
.min(candidate.settings.max_object_bytes)
};
let required_growth = target_capacity.saturating_sub(before);
if required_growth > 0
&& !candidate
.fill_gauge
@@ -246,7 +282,9 @@ impl HttpBody for FillBody {
} else if required_growth > 0
&& candidate
.data
.try_reserve_exact(needed.saturating_sub(candidate.data.len()))
.try_reserve_exact(
target_capacity.saturating_sub(candidate.data.len()),
)
.is_err()
{
candidate.cache.record_reason(