mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
485 lines
14 KiB
Go
485 lines
14 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/sessions"
|
|
)
|
|
|
|
// TestSessionMapBounds_HardLimitEnforcement tests that the session map enforces hard limits
|
|
// and prevents unbounded memory growth
|
|
func TestSessionMapBounds_HardLimitEnforcement(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
maxSessions int
|
|
sessionCount int
|
|
expectEviction bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "within_limit",
|
|
maxSessions: 100,
|
|
sessionCount: 50,
|
|
expectEviction: false,
|
|
description: "Sessions within limit should not trigger eviction",
|
|
},
|
|
{
|
|
name: "at_limit",
|
|
maxSessions: 100,
|
|
sessionCount: 100,
|
|
expectEviction: false,
|
|
description: "Sessions at exact limit should not trigger eviction",
|
|
},
|
|
{
|
|
name: "exceeds_limit",
|
|
maxSessions: 100,
|
|
sessionCount: 150,
|
|
expectEviction: true,
|
|
description: "Sessions exceeding limit should trigger eviction",
|
|
},
|
|
{
|
|
name: "small_limit",
|
|
maxSessions: 10,
|
|
sessionCount: 20,
|
|
expectEviction: true,
|
|
description: "Small limit should be strictly enforced",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create chunk manager with custom limits
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = tt.maxSessions
|
|
|
|
// Record initial memory
|
|
runtime.GC()
|
|
var m1 runtime.MemStats
|
|
runtime.ReadMemStats(&m1)
|
|
|
|
// Create sessions by storing them in the session map
|
|
for i := 0; i < tt.sessionCount; i++ {
|
|
sessionKey := generateSessionKey(i)
|
|
|
|
// Create a mock session entry
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
cm.mutex.Unlock()
|
|
|
|
// Trigger cleanup every 10 sessions to test enforcement
|
|
if i%10 == 9 {
|
|
cm.CleanupExpiredSessions()
|
|
}
|
|
}
|
|
|
|
// Force final cleanup to enforce limits
|
|
cm.CleanupExpiredSessions()
|
|
|
|
// Check final session count
|
|
cm.mutex.RLock()
|
|
finalSessionCount := len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
|
|
// Verify hard limit enforcement
|
|
if finalSessionCount > tt.maxSessions {
|
|
t.Errorf("Hard limit not enforced: %s\n"+
|
|
"Max sessions: %d\n"+
|
|
"Final session count: %d\n"+
|
|
"Expected eviction: %v",
|
|
tt.description, tt.maxSessions, finalSessionCount, tt.expectEviction)
|
|
}
|
|
|
|
// Verify eviction occurred if expected
|
|
if tt.expectEviction && finalSessionCount >= tt.sessionCount {
|
|
t.Errorf("Expected eviction did not occur: %s\n"+
|
|
"Created sessions: %d\n"+
|
|
"Final sessions: %d",
|
|
tt.description, tt.sessionCount, finalSessionCount)
|
|
}
|
|
|
|
// Record final memory
|
|
runtime.GC()
|
|
var m2 runtime.MemStats
|
|
runtime.ReadMemStats(&m2)
|
|
memoryGrowth := m2.Alloc - m1.Alloc
|
|
|
|
t.Logf("Test %s: Created %d sessions, Final count: %d, Memory growth: %d bytes",
|
|
tt.name, tt.sessionCount, finalSessionCount, memoryGrowth)
|
|
|
|
// Verify memory growth is bounded
|
|
maxExpectedMemoryPerSession := int64(1024) // 1KB per session
|
|
maxExpectedMemory := int64(tt.maxSessions) * maxExpectedMemoryPerSession
|
|
if int64(memoryGrowth) > maxExpectedMemory*2 { // Allow 2x tolerance
|
|
t.Errorf("Memory growth exceeds expected bounds: %d bytes (max expected: %d)",
|
|
memoryGrowth, maxExpectedMemory)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSessionMapBounds_EmergencyCleanup tests that emergency cleanup triggers when approaching limits
|
|
func TestSessionMapBounds_EmergencyCleanup(t *testing.T) {
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = 50
|
|
|
|
// Fill sessions to near capacity
|
|
nearCapacity := cm.maxSessions - 5
|
|
for i := 0; i < nearCapacity; i++ {
|
|
sessionKey := generateSessionKey(i)
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now().Add(time.Duration(-i) * time.Hour), // Vary ages for LRU
|
|
}
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
// Add some expired sessions that should be cleaned up
|
|
expiredCount := 10
|
|
for i := 0; i < expiredCount; i++ {
|
|
sessionKey := generateExpiredSessionKey(i)
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(-24 * time.Hour), // Expired
|
|
LastUsed: time.Now().Add(-48 * time.Hour),
|
|
}
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
// Record state before emergency cleanup
|
|
cm.mutex.RLock()
|
|
beforeCleanup := len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
|
|
// Trigger emergency cleanup
|
|
cm.CleanupExpiredSessions()
|
|
|
|
// Check that expired sessions were removed
|
|
cm.mutex.RLock()
|
|
afterCleanup := len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
|
|
cleanedUp := beforeCleanup - afterCleanup
|
|
if cleanedUp < expiredCount {
|
|
t.Errorf("Emergency cleanup insufficient: cleaned %d sessions, expected at least %d",
|
|
cleanedUp, expiredCount)
|
|
}
|
|
|
|
// Verify we're still within limits
|
|
if afterCleanup > cm.maxSessions {
|
|
t.Errorf("Emergency cleanup failed to enforce limits: %d sessions > %d max",
|
|
afterCleanup, cm.maxSessions)
|
|
}
|
|
|
|
t.Logf("Emergency cleanup: Before: %d, After: %d, Cleaned: %d",
|
|
beforeCleanup, afterCleanup, cleanedUp)
|
|
}
|
|
|
|
// TestSessionMapBounds_EvictionUnderHighLoad tests session eviction under high concurrent load
|
|
func TestSessionMapBounds_EvictionUnderHighLoad(t *testing.T) {
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = 100
|
|
|
|
// Record initial memory
|
|
runtime.GC()
|
|
var m1 runtime.MemStats
|
|
runtime.ReadMemStats(&m1)
|
|
|
|
const numGoroutines = 10
|
|
const sessionsPerGoroutine = 50
|
|
var wg sync.WaitGroup
|
|
|
|
// Create sessions concurrently to simulate high load
|
|
for g := 0; g < numGoroutines; g++ {
|
|
wg.Add(1)
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < sessionsPerGoroutine; i++ {
|
|
sessionKey := generateConcurrentSessionKey(goroutineID, i)
|
|
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
|
|
// Randomly trigger cleanup to test concurrent access
|
|
if i%10 == goroutineID%10 {
|
|
cm.mutex.Unlock()
|
|
cm.CleanupExpiredSessions()
|
|
} else {
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
// Small delay to increase concurrency contention
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}(g)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Final cleanup
|
|
cm.CleanupExpiredSessions()
|
|
|
|
// Verify limits are still enforced
|
|
cm.mutex.RLock()
|
|
finalCount := len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
|
|
if finalCount > cm.maxSessions {
|
|
t.Errorf("High load caused limit breach: %d sessions > %d max", finalCount, cm.maxSessions)
|
|
}
|
|
|
|
// Check memory usage
|
|
runtime.GC()
|
|
var m2 runtime.MemStats
|
|
runtime.ReadMemStats(&m2)
|
|
memoryGrowth := m2.Alloc - m1.Alloc
|
|
|
|
t.Logf("High load test: Created %d total sessions, Final count: %d, Memory growth: %d bytes",
|
|
numGoroutines*sessionsPerGoroutine, finalCount, memoryGrowth)
|
|
|
|
// Verify memory is bounded
|
|
maxExpectedMemory := int64(cm.maxSessions * 2048) // 2KB per session
|
|
if int64(memoryGrowth) > maxExpectedMemory {
|
|
t.Errorf("Memory growth under high load: %d bytes > %d expected max",
|
|
memoryGrowth, maxExpectedMemory)
|
|
}
|
|
}
|
|
|
|
// TestSessionMapBounds_NoMemoryGrowthBeyondLimits tests that memory doesn't grow beyond configured limits
|
|
func TestSessionMapBounds_NoMemoryGrowthBeyondLimits(t *testing.T) {
|
|
const maxSessions = 200
|
|
const testIterations = 1000 // Create way more sessions than limit
|
|
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = maxSessions
|
|
|
|
// Record baseline memory
|
|
runtime.GC()
|
|
runtime.GC()
|
|
var baseline runtime.MemStats
|
|
runtime.ReadMemStats(&baseline)
|
|
|
|
// Create sessions in waves, exceeding limits
|
|
for wave := 0; wave < 5; wave++ {
|
|
// Create burst of sessions
|
|
for i := 0; i < testIterations/5; i++ {
|
|
sessionKey := generateWaveSessionKey(wave, i)
|
|
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
cm.mutex.Unlock()
|
|
|
|
// Periodic cleanup
|
|
if i%50 == 49 {
|
|
cm.CleanupExpiredSessions()
|
|
}
|
|
}
|
|
|
|
// Force cleanup after each wave
|
|
cm.CleanupExpiredSessions()
|
|
|
|
// Check session count doesn't exceed limits
|
|
cm.mutex.RLock()
|
|
currentCount := len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
|
|
if currentCount > maxSessions {
|
|
t.Errorf("Session count exceeded limit in wave %d: %d > %d",
|
|
wave, currentCount, maxSessions)
|
|
}
|
|
|
|
// Check memory growth is bounded
|
|
runtime.GC()
|
|
var current runtime.MemStats
|
|
runtime.ReadMemStats(¤t)
|
|
memoryGrowth := current.Alloc - baseline.Alloc
|
|
|
|
// Memory should not grow linearly with total sessions created
|
|
maxExpectedMemory := uint64(maxSessions * 3072) // 3KB per session with overhead
|
|
if memoryGrowth > maxExpectedMemory {
|
|
t.Errorf("Memory growth exceeded bounds in wave %d: %d bytes > %d expected",
|
|
wave, memoryGrowth, maxExpectedMemory)
|
|
}
|
|
|
|
t.Logf("Wave %d: Sessions: %d, Memory growth: %d bytes",
|
|
wave, currentCount, memoryGrowth)
|
|
}
|
|
}
|
|
|
|
// TestSessionMapBounds_LRUEvictionOrder tests that LRU eviction maintains correct order
|
|
func TestSessionMapBounds_LRUEvictionOrder(t *testing.T) {
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = 10
|
|
|
|
// Create sessions with known access patterns
|
|
sessionOrder := make([]string, 0, 15)
|
|
|
|
// Create initial sessions
|
|
for i := 0; i < 15; i++ {
|
|
sessionKey := generateOrderedSessionKey(i)
|
|
sessionOrder = append(sessionOrder, sessionKey)
|
|
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now().Add(time.Duration(-i) * time.Minute), // Older sessions have earlier LastUsed
|
|
}
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
// Force eviction
|
|
cm.CleanupExpiredSessions()
|
|
|
|
// Check that oldest sessions were evicted
|
|
cm.mutex.RLock()
|
|
remainingSessions := make([]string, 0, len(cm.sessionMap))
|
|
for key := range cm.sessionMap {
|
|
remainingSessions = append(remainingSessions, key)
|
|
}
|
|
cm.mutex.RUnlock()
|
|
|
|
// Should have exactly maxSessions remaining
|
|
if len(remainingSessions) != cm.maxSessions {
|
|
t.Errorf("Incorrect number of sessions after eviction: got %d, expected %d",
|
|
len(remainingSessions), cm.maxSessions)
|
|
}
|
|
|
|
// Check that the most recently used sessions remain
|
|
// (sessions with lower indices have more recent LastUsed times)
|
|
expectedRemaining := sessionOrder[:cm.maxSessions]
|
|
for _, expectedKey := range expectedRemaining {
|
|
found := false
|
|
for _, remainingKey := range remainingSessions {
|
|
if remainingKey == expectedKey {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected session %s to remain after LRU eviction", expectedKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for generating unique session keys
|
|
|
|
func generateSessionKey(id int) string {
|
|
return "session_" + strings.Repeat("0", 5-len(string(rune(id)))) + string(rune('0'+id%10))
|
|
}
|
|
|
|
func generateExpiredSessionKey(id int) string {
|
|
return "expired_session_" + strings.Repeat("0", 5-len(string(rune(id)))) + string(rune('0'+id%10))
|
|
}
|
|
|
|
func generateConcurrentSessionKey(goroutineID, sessionID int) string {
|
|
return generateSessionKey(goroutineID*1000 + sessionID)
|
|
}
|
|
|
|
func generateWaveSessionKey(wave, id int) string {
|
|
return "wave_" + string(rune('0'+wave)) + "_" + generateSessionKey(id)
|
|
}
|
|
|
|
func generateOrderedSessionKey(id int) string {
|
|
return "ordered_" + strings.Repeat("0", 5-len(string(rune(id)))) + string(rune('0'+id%10))
|
|
}
|
|
|
|
// BenchmarkSessionMapBounds_EvictionPerformance benchmarks the performance of session eviction
|
|
func BenchmarkSessionMapBounds_EvictionPerformance(b *testing.B) {
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = 1000
|
|
|
|
// Pre-populate with sessions at capacity
|
|
for i := 0; i < cm.maxSessions; i++ {
|
|
sessionKey := generateSessionKey(i)
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now().Add(time.Duration(-i) * time.Minute),
|
|
}
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
// Add session that will trigger eviction
|
|
sessionKey := generateSessionKey(cm.maxSessions + i)
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
cm.mutex.Unlock()
|
|
|
|
// Force eviction
|
|
cm.CleanupExpiredSessions()
|
|
}
|
|
}
|
|
|
|
// BenchmarkSessionMapBounds_ConcurrentAccess benchmarks concurrent session access with bounds checking
|
|
func BenchmarkSessionMapBounds_ConcurrentAccess(b *testing.B) {
|
|
cm := NewChunkManager(nil)
|
|
cm.maxSessions = 500
|
|
|
|
// Pre-populate sessions
|
|
for i := 0; i < cm.maxSessions/2; i++ {
|
|
sessionKey := generateSessionKey(i)
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
cm.mutex.Unlock()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
i := 0
|
|
for pb.Next() {
|
|
sessionKey := generateSessionKey(i)
|
|
|
|
// Mix of operations: create, cleanup, access
|
|
switch i % 3 {
|
|
case 0:
|
|
cm.mutex.Lock()
|
|
cm.sessionMap[sessionKey] = &SessionEntry{
|
|
Session: &sessions.Session{Values: make(map[interface{}]interface{})},
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
LastUsed: time.Now(),
|
|
}
|
|
cm.mutex.Unlock()
|
|
case 1:
|
|
cm.CleanupExpiredSessions()
|
|
case 2:
|
|
cm.mutex.RLock()
|
|
_ = len(cm.sessionMap)
|
|
cm.mutex.RUnlock()
|
|
}
|
|
i++
|
|
}
|
|
})
|
|
}
|