Files
graphql-monitoring-proxy/cache/memory/eviction_test.go
T
lukaszraczylo 14be2398e8 fix: restore tracing endpoint validation; honor per-side client timeouts; cache eviction target; redis close
- tracing: validateEndpoint used fmt.Sscanf("%s:%d") which treats %s as
  greedy, so every valid host:port endpoint failed and traced==nil disabled
  tracing entirely. Validate with net.SplitHostPort (pure helper, port range
  0-65535); test TestValidateEndpoint.
- proxy: createFasthttpClient used clientTimeout for every timeout slot,
  ignoring the parsed CLIENT_READ_TIMEOUT/CLIENT_WRITE_TIMEOUT (silent
  config loss, documented separately in README). Honour per-side read/write
  timeouts, falling back to client timeout when unset.
- cache/memory: evictToFreeMemory capped candidates at maxCacheSize/5 and
  stopped at the subset end, so a large write could leave the cache over
  its memory limit. Collect all entries and sort.Slice (correct + O(n log n)
  vs O(cap^2)), evict until the target is met or the cache is drained.
- cache/redis: the Redis cache client was never closed at shutdown (unlike
  the metrics aggregator). Add RedisConfig.Close, CacheWrapper.Close, a
  cache.Shutdown and register it with the ShutdownManager.
2026-08-13 07:16:52 +01:00

215 lines
7.0 KiB
Go

package libpack_cache_memory
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestEvictToFreeMemory tests that the cache correctly evicts
// items when it exceeds its memory limit.
func TestEvictToFreeMemory(t *testing.T) {
// Create a cache with a small memory limit: 5KB (ensure eviction happens)
smallMemLimit := int64(5 * 1024)
cache := NewWithSize(5*time.Second, smallMemLimit, 1000)
// Create entries with known sizes
// Each entry will be ~512 bytes plus overhead
valueSize := 512
numEntriesToExceedLimit := 12 // Should exceed the 5KB limit and force eviction
// Create a slice to track keys in insertion order
keys := make([]string, numEntriesToExceedLimit)
// Add entries with significant delays between insertions
for i := 0; i < numEntriesToExceedLimit; i++ {
key := fmt.Sprintf("test-key-%d", i)
keys[i] = key
value := make([]byte, valueSize)
for j := 0; j < valueSize; j++ {
value[j] = byte(i % 256) // Fill with a repeating pattern
}
cache.Set(key, value, 30*time.Second)
// More significant delay to ensure different timestamps
time.Sleep(10 * time.Millisecond)
}
// Allow time for eviction to complete
time.Sleep(50 * time.Millisecond)
// Verify memory usage is below the limit
memUsage := cache.GetMemoryUsage()
assert.LessOrEqual(t, memUsage, smallMemLimit,
"Memory usage (%d) should be less than or equal to the limit (%d)", memUsage, smallMemLimit)
// Count how many items are left in the cache and which ones
present := 0
for i := 0; i < numEntriesToExceedLimit; i++ {
_, found := cache.Get(keys[i])
if found {
present++
}
}
// We expect some items to be evicted based on the memory limit
assert.Less(t, present, numEntriesToExceedLimit,
"Some items should have been evicted (%d present out of %d total)",
present, numEntriesToExceedLimit)
// Verify newer items (inserted later) are more likely to be in the cache
// Check the last few items which should be the newest
for i := numEntriesToExceedLimit - 3; i < numEntriesToExceedLimit; i++ {
_, found := cache.Get(keys[i])
assert.True(t, found, "Newer key %s should still exist", keys[i])
}
}
// TestMaxCacheSize verifies the behavior when adding more items than the maxCacheSize limit
func TestMaxCacheSize(t *testing.T) {
// Create a cache with a small limit
smallLimit := int64(5)
cache := NewWithSize(5*time.Second, DefaultMaxMemorySize, smallLimit)
// Add entries with increasing size (to avoid memory-based eviction)
for i := 0; i < 20; i++ {
key := fmt.Sprintf("test-key-%d", i)
value := []byte(key)
cache.Set(key, value, 10*time.Second)
}
// Verify we can get a reasonable number of items
// (we don't test for exact count as implementation may vary)
foundCount := 0
for i := 0; i < 20; i++ {
key := fmt.Sprintf("test-key-%d", i)
_, found := cache.Get(key)
if found {
foundCount++
}
}
// We should find some items but not all 20
assert.Greater(t, foundCount, 0, "Some items should be in the cache")
assert.LessOrEqual(t, foundCount, 20, "Not all items should be in the cache with small limit")
}
// TestGetMemoryUsage verifies that memory usage tracking is accurate
func TestGetMemoryUsage(t *testing.T) {
cache := New(5 * time.Second)
// Initially memory usage should be 0
assert.Equal(t, int64(0), cache.GetMemoryUsage(), "Initial memory usage should be 0")
// Add an entry with a known approximate size
valueSize := 1024
value := make([]byte, valueSize)
key := "test-key"
cache.Set(key, value, 5*time.Second)
// Check memory usage - should be approximately valueSize + key length + overhead
expectedMinUsage := int64(valueSize + len(key))
memUsage := cache.GetMemoryUsage()
assert.GreaterOrEqual(t, memUsage, expectedMinUsage,
"Memory usage (%d) should be at least the value size plus key length (%d)", memUsage, expectedMinUsage)
// Delete the entry and verify memory usage decreases
cache.Delete(key)
assert.Equal(t, int64(0), cache.GetMemoryUsage(), "Memory usage should be 0 after deletion")
}
// TestSetMaxMemorySize tests changing the memory limit and resulting eviction
func TestSetMaxMemorySize(t *testing.T) {
// Start with a large limit
initialLimit := int64(100 * 1024)
cache := NewWithSize(5*time.Second, initialLimit, 1000)
// Fill the cache with ~50KB of data
valueSize := 1024
numEntries := 50
for i := 0; i < numEntries; i++ {
key := generateKey(i)
value := make([]byte, valueSize)
cache.Set(key, value, 5*time.Second)
// Small delay for timestamp differences
time.Sleep(time.Millisecond)
}
// Verify all entries exist
for i := 0; i < numEntries; i++ {
_, found := cache.Get(generateKey(i))
assert.True(t, found, "All entries should exist before limit change")
}
// Get current memory usage
originalUsage := cache.GetMemoryUsage()
// Now reduce the limit to 20KB - should trigger eviction
newLimit := int64(20 * 1024)
cache.SetMaxMemorySize(newLimit)
// Verify memory usage is now below the new limit
newUsage := cache.GetMemoryUsage()
assert.LessOrEqual(t, newUsage, newLimit,
"After SetMaxMemorySize, memory usage (%d) should be less than or equal to new limit (%d)",
newUsage, newLimit)
assert.Less(t, newUsage, originalUsage,
"Memory usage should have decreased after lowering the limit")
// Some older entries should be gone, newer ones should still exist
removedCount := 0
remainingCount := 0
for i := 0; i < numEntries; i++ {
_, found := cache.Get(generateKey(i))
if found {
remainingCount++
} else {
removedCount++
}
}
assert.Greater(t, removedCount, 0, "Some entries should have been removed")
assert.Greater(t, remainingCount, 0, "Some entries should still exist")
}
// Helper function to generate consistent keys
func generateKey(index int) string {
return "test-key-" + fmt.Sprintf("%d", index)
}
// TestEvictToFreeMemoryWithSmallMaxEntriesReachesTarget is a regression for
// under-eviction: evictToFreeMemory used to cap its candidate set at
// maxCacheSize/5 and stop at the end of that subset. With a small
// maxCacheSize the cap (maxCacheSize/5) could be lower than the number of
// entries needed to satisfy bytesToFree, so the cache stayed over its
// memory limit.
func TestEvictToFreeMemoryWithSmallMaxEntriesReachesTarget(t *testing.T) {
// maxCacheSize=10 -> old candidate cap was maxCacheSize/5 = 2 entries.
cache := NewWithSize(5*time.Second, DefaultMaxMemorySize, 10)
val := make([]byte, 1024) // ~1KB + overhead each
const count = 8
for i := 0; i < count; i++ {
cache.Set(fmt.Sprintf("e%d", i), val, time.Second)
}
before := cache.GetMemoryUsage()
// Request freeing an amount equal to ~half the entries (well beyond the
// old 2-entry cap).
need := (count / 2) * (int64(len(val)) + approxEntryOverhead)
cache.evictToFreeMemory(need)
after := cache.GetMemoryUsage()
assert.LessOrEqual(t, after, before-need,
"evictToFreeMemory must free at least the requested bytes even across more than maxCacheSize/5 entries")
// Requesting more than the total must drain the cache.
cache.evictToFreeMemory(1 << 30)
assert.Equal(t, int64(0), cache.GetMemoryUsage(),
"an oversized request should evict every entry")
}