This is an automated email from the ASF dual-hosted git repository.
masaori335 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 6757a9e99a Fix LRU RAM cache seen filter never engaging below 100%
full (#13234)
6757a9e99a is described below
commit 6757a9e99a2d0b5f4e18590cbc8e7a41236757a4
Author: Phong Nguyen <[email protected]>
AuthorDate: Tue Jun 23 15:48:15 2026 -0700
Fix LRU RAM cache seen filter never engaging below 100% full (#13234)
proxy.config.cache.ram_cache.use_seen_filter values above 1 are
documented to turn on the seen filter once the cache is (N-1)/N full
(2 = 50%, 3 = 67%, ... 9 = 90%). The threshold was written as
bytes >= max_bytes * (1 - (1 / N)) with N an int, so 1 / N is integer
division and evaluates to 0 for every N > 1; the test became
bytes >= max_bytes and the filter only engaged when completely full.
A scan could therefore pollute a half-full cache.
Rewrite the comparison as bytes * N >= max_bytes * (N - 1), which is
exact in integer arithmetic and overflow-safe at realistic cache
sizes. Add ram_cache_lru_seen_filter, which fails on the old form
(20/20 unseen keys admitted at 60% full) and passes on the fix (0/20).
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
src/iocore/cache/CacheTest.cc | 70 +++++++++++++++++++++++++++++++++++++++++
src/iocore/cache/RamCacheLRU.cc | 9 ++++--
2 files changed, 76 insertions(+), 3 deletions(-)
diff --git a/src/iocore/cache/CacheTest.cc b/src/iocore/cache/CacheTest.cc
index 3d39abe8cc..d171ac6b1f 100644
--- a/src/iocore/cache/CacheTest.cc
+++ b/src/iocore/cache/CacheTest.cc
@@ -710,3 +710,73 @@ REGRESSION_TEST(ram_cache)(RegressionTest *t, int level,
int *pstatus)
cache_config_ram_cache_s3fifo_promote_threshold = saved_promote;
}
}
+
+// Verifies the tiered LRU seen filter engages at the documented fill level.
With
+// proxy.config.cache.ram_cache.use_seen_filter = N (> 1) the filter must turn
on once the cache
+// is (N - 1)/N full (N = 2 -> 50%). An integer-division bug (1 / N == 0) made
it turn on only at
+// 100% full, so a scan could pollute a half-full cache.
+REGRESSION_TEST(ram_cache_lru_seen_filter)(RegressionTest *t, int level, int
*pstatus)
+{
+ if (REGRESSION_TEST_NIGHTLY > level) {
+ *pstatus = REGRESSION_TEST_PASSED;
+ return;
+ }
+ if (cacheProcessor.IsCacheEnabled() != CacheInitState::INITIALIZED) {
+ rprintf(t, "cache not initialized");
+ *pstatus = REGRESSION_TEST_FAILED;
+ return;
+ }
+
+ int const saved_filter =
cache_config_ram_cache_use_seen_filter;
+ cache_config_ram_cache_use_seen_filter = 2; // engage once the cache is 50%
full
+
+ CacheKey key;
+ StripeSM *stripe = theCache->key_to_stripe(&key, "example.com"sv);
+ int64_t cache_size = 1LL << 21; // 2 MB
+ RamCache *cache = new_RamCacheLRU();
+ cache->init(cache_size, stripe);
+
+ std::vector<Ptr<IOBufferData>> keep;
+ auto put = [&](uint64_t n) {
+ CryptoHash hash;
+ hash.u64[0] = (n << 32) + n;
+ hash.u64[1] = (n << 32) + n;
+ IOBufferData *d = THREAD_ALLOC(ioDataAllocator, this_thread());
+ d->alloc(BUFFER_SIZE_INDEX_16K);
+ memset(d->data(), 0, d->block_size());
+ keep.push_back(make_ptr(d));
+ cache->put(&hash, d, d->block_size());
+ };
+ auto resident = [&](uint64_t n) -> bool {
+ CryptoHash hash;
+ hash.u64[0] = (n << 32) + n;
+ hash.u64[1] = (n << 32) + n;
+ Ptr<IOBufferData> got;
+ return cache->get(&hash, &got);
+ };
+
+ // Fill to ~60% (above the 50% threshold). Put each key twice so it is
admitted even once the
+ // filter is active (the first Put records "seen", the second admits).
+ int const obj = BUFFER_SIZE_FOR_INDEX(BUFFER_SIZE_INDEX_16K);
+ int const fill_n = static_cast<int>((cache_size * 6 / 10) / obj);
+ for (int i = 0; i < fill_n; i++) {
+ put(1000 + i);
+ put(1000 + i);
+ }
+
+ // Above the threshold, single-Put (unseen) keys must be filtered, not
admitted. Use a batch to
+ // be robust against the occasional seen-filter hash collision.
+ int admitted = 0;
+ for (int i = 0; i < 20; i++) {
+ put(900000 + i);
+ if (resident(900000 + i)) {
+ admitted++;
+ }
+ }
+
+ rprintf(t, "RamCache LRU seen filter: %d/20 single-seen keys admitted at
~60%% full (expect ~0)\n", admitted);
+
+ cache_config_ram_cache_use_seen_filter = saved_filter;
+ keep.clear();
+ *pstatus = (admitted <= 2) ? REGRESSION_TEST_PASSED : REGRESSION_TEST_FAILED;
+}
diff --git a/src/iocore/cache/RamCacheLRU.cc b/src/iocore/cache/RamCacheLRU.cc
index ca3d8a384a..6e392338be 100644
--- a/src/iocore/cache/RamCacheLRU.cc
+++ b/src/iocore/cache/RamCacheLRU.cc
@@ -190,9 +190,12 @@ RamCacheLRU::put(CryptoHash *key, IOBufferData *data,
[[maybe_unused]] uint32_t
}
uint32_t i = key->slice32(3) % nbuckets;
if ((cache_config_ram_cache_use_seen_filter == 1) ||
- // If proxy.config.cache.ram_cache.use_seen_filter is > 1, and the
cache is more than <n>% full, then use the seen filter.
- // <n>% is calculated based on this setting, with 2 == 50%, 3 == 67%, 4
== 75%, up to 9 == 90%.
- ((cache_config_ram_cache_use_seen_filter > 1) && (bytes >= max_bytes *
(1 - (1 / cache_config_ram_cache_use_seen_filter))))) {
+ // For use_seen_filter > 1, only apply the filter once the cache is more
than <n>% full:
+ // 2 == 50%, 3 == 67%, 4 == 75%, up to 9 == 90%. Written as bytes * N >=
max_bytes * (N - 1)
+ // rather than bytes >= max_bytes * (1 - 1 / N); the latter evaluates 1
/ N in integer
+ // arithmetic (0 for every N > 1), so the filter only ever engaged at
100% full.
+ ((cache_config_ram_cache_use_seen_filter > 1) &&
+ (bytes * cache_config_ram_cache_use_seen_filter >= max_bytes *
(cache_config_ram_cache_use_seen_filter - 1)))) {
uint32_t j = key->slice32(3) % (nbuckets * 2); // The seen filter bucket
size is 2x
if (!seen[j]) {