perf(cache): sort-based eviction and consistent non-positive TTL handling

The count-based evictOldest used an O(n squared) selection sort with a
candidate cap over an unordered map range, so it could evict the wrong
entries. It now collects all entries and sorts by expiry, matching the
memory-based evictor.

A non-positive CACHE_TTL behaved differently per backend: the standard
memory cache panicked, Redis stored a permanent key, and the LRU cache
expired entries immediately. Each backend now clamps a non-positive TTL
to a shared default at its Set boundary. The nil-logger fallback in the
Redis wrapper now uses the constructor so it cannot panic.
This commit is contained in:
2026-08-14 11:09:57 +01:00
parent 1cab28b054
commit 159faa68e0
9 changed files with 308 additions and 25 deletions
+26
View File
@@ -106,6 +106,32 @@ func (suite *LRUMemoryCacheTestSuite) TestExpiration() {
suite.Nil(val)
}
// TestNonPositiveTTL_ClampedToDefaultTTL verifies the C11 cross-backend
// rule: a non-positive TTL is clamped to defaultTTL instead of stamping the
// entry as already-expired (which used to make every read miss
// immediately, unlike the Redis backend which used to keep the entry
// forever).
func (suite *LRUMemoryCacheTestSuite) TestNonPositiveTTL_ClampedToDefaultTTL() {
cache := NewLRUMemoryCache(1024*1024, 100)
cache.Set("zero-ttl", []byte("value"), 0)
val, found := cache.Get("zero-ttl")
suite.True(found, "zero TTL should be clamped to defaultTTL, not stored as already-expired")
suite.Equal([]byte("value"), val)
cache.Set("negative-ttl", []byte("value"), -1*time.Second)
val, found = cache.Get("negative-ttl")
suite.True(found, "negative TTL should be clamped to defaultTTL, not stored as already-expired")
suite.Equal([]byte("value"), val)
cache.mu.RLock()
entry, ok := cache.entries["zero-ttl"]
cache.mu.RUnlock()
suite.Require().True(ok)
suite.WithinDuration(time.Now().Add(defaultTTL), entry.expiresAt, 2*time.Second,
"clamped entry should expire ~defaultTTL from now")
}
func (suite *LRUMemoryCacheTestSuite) TestEvictionByCount() {
cache := NewLRUMemoryCache(1024*1024, 3) // Max 3 entries