This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 08:53:55 +02:00
1 parent 1431dd47f0
commit f3dd34dae4
42 files changed
+950 -133

No files matched your search

+14
View File
@@ -103,6 +103,20 @@ impl Cache {
{
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
store.generation = store.generation.wrapping_add(1);
// This global publication barrier invalidates every older
// ticket and pending COLD demotion. Per-key generations only
// need to cover the current purge, not every key ever
// purged over the lifetime of the process. Without clearing
// them, repeated PURGEs of distinct URLs grow this map
// indefinitely even after the objects have been evicted.
// Release an unusually large index's backing allocation
// after mass invalidations instead of retaining its peak
// bucket capacity for the rest of the process lifetime.
if store.key_generations.capacity() > 256 {
store.key_generations = Default::default();
} else {
store.key_generations.clear();
}
}
let affected = self.purge_matcher(&matcher);
self.counters.purges.fetch_add(1, Ordering::Relaxed);
+74
View File
@@ -395,3 +395,77 @@
confirmed_prefix.confirm_wide = true;
assert!(cache.invalidate(confirmed_prefix).unwrap().ban_id.is_some());
}
#[tokio::test]
async fn many_distinct_selective_purges_do_not_retain_tombstones_forever() {
let cache = Cache::new(22);
store(
&cache,
"app",
"memory.example",
"/stable",
HeaderMap::new(),
&[],
"unchanged",
)
.await;
// A production cache may see an unbounded stream of PURGEs for URLs
// that will never be cached again. The invalidated-key index must
// not remember all of those removed objects indefinitely.
for index in 0..512 {
let target = format!("/unique-{index}");
let old = store(
&cache,
"app",
"memory.example",
&target,
HeaderMap::new(),
&[],
"x",
)
.await;
assert!(cache.ticket_current(&old));
let result = cache
.invalidate(invalidation(
InvalidationOperation::Purge,
InvalidationSelector::Exact,
&target,
Some("memory.example"),
Some("app"),
))
.unwrap();
assert_eq!(result.affected_entries, 1);
assert!(!cache.ticket_current(&old), "pre-purge ticket must remain stale");
let state = cache.store.lock().unwrap_or_else(|e| e.into_inner());
assert!(
state.key_generations.len() <= 1,
"key generations must not accumulate for already purged URLs"
);
}
// A new ticket after the invalidation barrier is valid and can fill
// the same URL again, preserving ordinary cache behavior.
let new = store(
&cache,
"app",
"memory.example",
"/unique-0",
HeaderMap::new(),
&[],
"new",
)
.await;
assert!(cache.ticket_current(&new));
assert!(cache.get(&new).is_some());
let stable = cache
.ticket_scoped(
22,
"app",
"http",
"memory.example",
"origin@127.0.0.1:8080",
"/stable",
&HeaderMap::new(),
)
.unwrap();
assert!(cache.get(&stable).is_some(), "unrelated entries must survive");
}