mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
337 lines
8.7 KiB
Go
337 lines
8.7 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCacheMemoryLeaks tests various cache scenarios for memory leaks
|
|
func TestCacheMemoryLeaks(t *testing.T) {
|
|
t.Run("Cache doesn't release expired items memory", func(t *testing.T) {
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
cache := NewCache()
|
|
defer cache.Close()
|
|
|
|
// Add many large items with short expiration
|
|
largeData := make([]byte, 1024*1024) // 1MB
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("key-%d", i)
|
|
// Items expire in 1 second
|
|
cache.Set(key, largeData, 1*time.Second)
|
|
}
|
|
|
|
// Wait for items to expire
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Force cleanup
|
|
cache.Cleanup()
|
|
|
|
// Check memory after cleanup
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
afterCleanupAlloc := m.Alloc
|
|
|
|
allocIncrease := float64(afterCleanupAlloc-baselineAlloc) / 1024 / 1024
|
|
t.Logf("Memory after adding and expiring 100MB of data: %.2f MB", allocIncrease)
|
|
|
|
// Memory should be mostly released after cleanup
|
|
if allocIncrease > 10.0 {
|
|
t.Errorf("Cache retains too much memory after cleanup: %.2f MB", allocIncrease)
|
|
}
|
|
})
|
|
|
|
t.Run("Token blacklist unbounded growth", func(t *testing.T) {
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
blacklist := NewCache()
|
|
blacklist.SetMaxSize(1000) // Limit size
|
|
defer blacklist.Close()
|
|
|
|
// Simulate continuous token blacklisting
|
|
for i := 0; i < 10000; i++ {
|
|
token := fmt.Sprintf("token-%d", i)
|
|
// All tokens expire in 24 hours (typical blacklist duration)
|
|
blacklist.Set(token, true, 24*time.Hour)
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
currentAlloc := m.Alloc
|
|
|
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
|
t.Logf("Memory after adding 10000 blacklisted tokens (max 1000): %.2f MB", allocIncrease)
|
|
|
|
// Should respect max size limit
|
|
if len(blacklist.items) > 1000 {
|
|
t.Errorf("Blacklist exceeded max size: %d items", len(blacklist.items))
|
|
}
|
|
|
|
// Memory should be bounded
|
|
if allocIncrease > 5.0 {
|
|
t.Errorf("Blacklist uses too much memory: %.2f MB for max 1000 items", allocIncrease)
|
|
}
|
|
})
|
|
|
|
t.Run("Replay cache with high JTI volume", func(t *testing.T) {
|
|
initReplayCache()
|
|
defer cleanupReplayCache()
|
|
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
// Simulate high volume of JTIs
|
|
for i := 0; i < 20000; i++ {
|
|
jti := fmt.Sprintf("jti-%d", i)
|
|
replayCacheMu.Lock()
|
|
if replayCache != nil {
|
|
// JTIs expire after token expiry (typically 1 hour)
|
|
replayCache.Set(jti, true, 1*time.Hour)
|
|
}
|
|
replayCacheMu.Unlock()
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
currentAlloc := m.Alloc
|
|
|
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
|
t.Logf("Memory after adding 20000 JTIs (max 10000): %.2f MB", allocIncrease)
|
|
|
|
// Check size limit is enforced
|
|
replayCacheMu.RLock()
|
|
cacheSize := 0
|
|
if replayCache != nil {
|
|
cacheSize = len(replayCache.items)
|
|
}
|
|
replayCacheMu.RUnlock()
|
|
|
|
if cacheSize > 10000 {
|
|
t.Errorf("Replay cache exceeded max size: %d items", cacheSize)
|
|
}
|
|
|
|
// Memory should be bounded
|
|
if allocIncrease > 10.0 {
|
|
t.Errorf("Replay cache uses too much memory: %.2f MB for max 10000 items", allocIncrease)
|
|
}
|
|
})
|
|
|
|
t.Run("Cache cleanup interval effectiveness", func(t *testing.T) {
|
|
// Create a cache with custom settings - don't use NewCache to avoid default cleanup
|
|
cache := &Cache{
|
|
items: make(map[string]CacheItem, DefaultMaxSize),
|
|
order: list.New(),
|
|
elems: make(map[string]*list.Element, DefaultMaxSize),
|
|
maxSize: DefaultMaxSize,
|
|
autoCleanupInterval: 200 * time.Millisecond, // Fast cleanup for test
|
|
logger: newNoOpLogger(),
|
|
}
|
|
|
|
// Start cleanup with our custom interval
|
|
cache.startAutoCleanup()
|
|
defer cache.Close()
|
|
|
|
// Add expired items
|
|
for i := 0; i < 1000; i++ {
|
|
key := fmt.Sprintf("key-%d", i)
|
|
cache.Set(key, "data", 50*time.Millisecond) // Very short expiry
|
|
}
|
|
|
|
// Wait for items to expire and cleanup to run (at least 2 cleanup cycles)
|
|
time.Sleep(600 * time.Millisecond)
|
|
|
|
// Manually trigger cleanup to ensure it runs
|
|
cache.Cleanup()
|
|
|
|
// Check that expired items are removed
|
|
cache.mutex.RLock()
|
|
remainingItems := len(cache.items)
|
|
cache.mutex.RUnlock()
|
|
|
|
t.Logf("Remaining items after auto cleanup: %d", remainingItems)
|
|
|
|
if remainingItems > 100 {
|
|
t.Errorf("Auto cleanup not effective: %d items remain", remainingItems)
|
|
}
|
|
})
|
|
|
|
t.Run("Concurrent cache operations memory stability", func(t *testing.T) {
|
|
cache := NewCache()
|
|
defer cache.Close()
|
|
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
var wg sync.WaitGroup
|
|
stop := make(chan struct{})
|
|
|
|
// Writers continuously add items
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 1000; j++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
key := fmt.Sprintf("writer-%d-%d", id, j)
|
|
cache.Set(key, "data", 1*time.Second)
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Readers continuously read items
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 1000; j++ {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
key := fmt.Sprintf("writer-%d-%d", id%5, j)
|
|
cache.Get(key)
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Let it run for a bit
|
|
time.Sleep(5 * time.Second)
|
|
close(stop)
|
|
wg.Wait()
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
finalAlloc := m.Alloc
|
|
|
|
// Handle potential underflow
|
|
var allocIncrease float64
|
|
if finalAlloc > baselineAlloc {
|
|
allocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
|
} else {
|
|
allocIncrease = -float64(baselineAlloc-finalAlloc) / 1024 / 1024
|
|
}
|
|
t.Logf("Memory increase under concurrent load: %.2f MB", allocIncrease)
|
|
|
|
if allocIncrease > 5.0 {
|
|
t.Errorf("Memory leak under concurrent operations: %.2f MB", allocIncrease)
|
|
}
|
|
})
|
|
|
|
t.Run("LRU eviction memory release", func(t *testing.T) {
|
|
cache := NewCache()
|
|
cache.SetMaxSize(100) // Small cache
|
|
defer cache.Close()
|
|
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
// Add many items to trigger eviction
|
|
for i := 0; i < 1000; i++ {
|
|
key := fmt.Sprintf("key-%d", i)
|
|
data := make([]byte, 10240) // 10KB per item
|
|
cache.Set(key, data, 1*time.Hour)
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
afterEvictionAlloc := m.Alloc
|
|
|
|
allocIncrease := float64(afterEvictionAlloc-baselineAlloc) / 1024 / 1024
|
|
t.Logf("Memory after LRU eviction (1000 items, max 100): %.2f MB", allocIncrease)
|
|
|
|
// Should only keep 100 items worth of memory
|
|
if allocIncrease > 2.0 { // 100 * 10KB = ~1MB
|
|
t.Errorf("LRU eviction doesn't release memory properly: %.2f MB", allocIncrease)
|
|
}
|
|
|
|
// Verify cache size
|
|
if len(cache.items) > 100 {
|
|
t.Errorf("Cache size exceeded limit: %d items", len(cache.items))
|
|
}
|
|
})
|
|
|
|
t.Run("Token cache with claims memory", func(t *testing.T) {
|
|
tokenCache := NewTokenCache()
|
|
defer tokenCache.Close()
|
|
|
|
runtime.GC()
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
baselineAlloc := m.Alloc
|
|
|
|
// Add tokens with large claims
|
|
for i := 0; i < 1000; i++ {
|
|
token := fmt.Sprintf("token-%d", i)
|
|
claims := map[string]interface{}{
|
|
"sub": fmt.Sprintf("user-%d", i),
|
|
"email": fmt.Sprintf("user%[email protected]", i),
|
|
"groups": make([]string, 100), // Large groups list
|
|
"data": make([]byte, 1024), // Extra data
|
|
}
|
|
tokenCache.Set(token, claims, 1*time.Hour)
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
currentAlloc := m.Alloc
|
|
|
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
|
t.Logf("Memory after adding 1000 tokens with large claims: %.2f MB", allocIncrease)
|
|
|
|
// Check if memory is reasonable
|
|
if allocIncrease > 20.0 {
|
|
t.Errorf("Token cache uses excessive memory: %.2f MB", allocIncrease)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCacheEvictionBug tests the inefficient eviction in evictOldest
|
|
func TestCacheEvictionBug(t *testing.T) {
|
|
t.Run("evictOldest scans entire list", func(t *testing.T) {
|
|
cache := NewCache()
|
|
cache.SetMaxSize(100)
|
|
defer cache.Close()
|
|
|
|
// Fill cache with non-expired items
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("key-%d", i)
|
|
cache.Set(key, "data", 1*time.Hour) // Long expiry
|
|
}
|
|
|
|
// Try to add one more item to trigger eviction
|
|
start := time.Now()
|
|
cache.Set("trigger", "data", 1*time.Hour)
|
|
elapsed := time.Since(start)
|
|
|
|
t.Logf("Time to evict and add one item: %v", elapsed)
|
|
|
|
// Should be fast even with full cache
|
|
if elapsed > 10*time.Millisecond {
|
|
t.Errorf("Eviction too slow, possibly scanning entire list: %v", elapsed)
|
|
}
|
|
})
|
|
}
|