mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-03 07:44:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910cbaf92a | ||
|
|
972993980e |
@@ -921,82 +921,6 @@ func TestMemoryTestOrchestrator_StopLeakDetection_CoverageBoost(t *testing.T) {
|
||||
// CHUNK MANAGER TESTS
|
||||
// =============================================================================
|
||||
|
||||
func TestChunkManager_GetSessionCount_CoverageBoost(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
cm := NewChunkManager(logger)
|
||||
if cm == nil {
|
||||
t.Fatal("Expected non-nil ChunkManager")
|
||||
}
|
||||
defer cm.Shutdown()
|
||||
|
||||
// Test GetSessionCount
|
||||
count := cm.GetSessionCount()
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 sessions, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkManager_GetMemoryStats_CoverageBoost(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
cm := NewChunkManager(logger)
|
||||
if cm == nil {
|
||||
t.Fatal("Expected non-nil ChunkManager")
|
||||
}
|
||||
defer cm.Shutdown()
|
||||
|
||||
// Test GetMemoryStats
|
||||
stats := cm.GetMemoryStats()
|
||||
if stats == nil {
|
||||
t.Fatal("Expected non-nil stats")
|
||||
}
|
||||
|
||||
// Verify expected keys exist
|
||||
if _, ok := stats["active_sessions"]; !ok {
|
||||
t.Error("Expected active_sessions key in stats")
|
||||
}
|
||||
if _, ok := stats["max_sessions"]; !ok {
|
||||
t.Error("Expected max_sessions key in stats")
|
||||
}
|
||||
if _, ok := stats["bytes_allocated"]; !ok {
|
||||
t.Error("Expected bytes_allocated key in stats")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkManager_CanCreateSession_CoverageBoost(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
cm := NewChunkManager(logger)
|
||||
if cm == nil {
|
||||
t.Fatal("Expected non-nil ChunkManager")
|
||||
}
|
||||
defer cm.Shutdown()
|
||||
|
||||
// Test CanCreateSession - should be true initially
|
||||
canCreate, err := cm.CanCreateSession()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !canCreate {
|
||||
t.Error("Expected CanCreateSession to return true when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkManager_EmergencyCleanup_CoverageBoost(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
cm := NewChunkManager(logger)
|
||||
if cm == nil {
|
||||
t.Fatal("Expected non-nil ChunkManager")
|
||||
}
|
||||
defer cm.Shutdown()
|
||||
|
||||
// Test EmergencyCleanup - should not panic on empty session map
|
||||
cm.EmergencyCleanup()
|
||||
|
||||
// Verify no sessions exist
|
||||
if cm.GetSessionCount() != 0 {
|
||||
t.Error("Expected 0 sessions after cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkManager_CleanupExpiredSessions_CoverageBoost(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
cm := NewChunkManager(logger)
|
||||
@@ -1008,8 +932,7 @@ func TestChunkManager_CleanupExpiredSessions_CoverageBoost(t *testing.T) {
|
||||
// Test CleanupExpiredSessions - should not panic on empty session map
|
||||
cm.CleanupExpiredSessions()
|
||||
|
||||
// Verify no sessions exist
|
||||
if cm.GetSessionCount() != 0 {
|
||||
if len(cm.sessionMap) != 0 {
|
||||
t.Error("Expected 0 sessions after cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ type TokenConfig struct {
|
||||
// Global session tracking to prevent memory leaks across all instances
|
||||
var (
|
||||
globalSessionCount int64 = 0
|
||||
globalMaxSessions int64 = 5000 // CRITICAL FIX: Global limit of 5000 total sessions
|
||||
)
|
||||
|
||||
// ResetGlobalSessionCounters resets global session tracking for testing
|
||||
@@ -1243,226 +1242,4 @@ func (cm *ChunkManager) CleanupExpiredSessions(force ...bool) {
|
||||
cm.logger.Debugf("Cleaned up %d expired sessions, freed %d bytes",
|
||||
len(expiredKeys), totalBytesFreed)
|
||||
}
|
||||
|
||||
// Enforce max sessions limit
|
||||
if len(cm.sessionMap) > cm.maxSessions {
|
||||
cm.enforceSessionLimit()
|
||||
}
|
||||
}
|
||||
|
||||
// enforceSessionLimit removes oldest sessions when limit is exceeded
|
||||
// Uses partial sort (O(n) for finding k smallest) instead of full sort (O(n log n))
|
||||
func (cm *ChunkManager) enforceSessionLimit() {
|
||||
currentLocal := len(cm.sessionMap)
|
||||
currentGlobal := atomic.LoadInt64(&globalSessionCount)
|
||||
|
||||
// CRITICAL FIX: Aggressive eviction when approaching limits
|
||||
shouldEvict := false
|
||||
targetCapacity := cm.maxSessions
|
||||
|
||||
// Check global limit first (more critical)
|
||||
if currentGlobal >= globalMaxSessions {
|
||||
shouldEvict = true
|
||||
targetCapacity = cm.maxSessions / 4 // Aggressive reduction to 25%
|
||||
} else if currentGlobal >= globalMaxSessions*8/10 { // 80% of global
|
||||
shouldEvict = true
|
||||
targetCapacity = cm.maxSessions / 2 // Reduce to 50%
|
||||
} else if currentLocal >= cm.maxSessions {
|
||||
shouldEvict = true
|
||||
targetCapacity = cm.maxSessions * 3 / 4 // Reduce to 75%
|
||||
}
|
||||
|
||||
if !shouldEvict {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate how many sessions to remove
|
||||
excessCount := currentLocal - targetCapacity
|
||||
if excessCount <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Use partial selection instead of full sort for better performance
|
||||
// For finding k oldest sessions, we only need O(n) operations
|
||||
keysToRemove := cm.findOldestSessions(excessCount)
|
||||
|
||||
totalBytesFreed := int64(0)
|
||||
removedCount := int64(0)
|
||||
|
||||
for _, key := range keysToRemove {
|
||||
if entry, exists := cm.sessionMap[key]; exists {
|
||||
totalBytesFreed += entry.SizeEstimate
|
||||
atomic.AddInt64(&cm.bytesAllocated, -entry.SizeEstimate)
|
||||
removedCount++
|
||||
}
|
||||
delete(cm.sessionMap, key)
|
||||
}
|
||||
|
||||
// Update global count
|
||||
if removedCount > 0 {
|
||||
atomic.AddInt64(&globalSessionCount, -removedCount)
|
||||
}
|
||||
|
||||
cm.logger.Infof("Enforced session limit: removed %d excess sessions, freed %d bytes",
|
||||
len(keysToRemove), totalBytesFreed)
|
||||
}
|
||||
|
||||
// findOldestSessions returns keys of the k oldest sessions efficiently
|
||||
// Uses a simple approach: find the kth oldest timestamp, then collect all older entries
|
||||
func (cm *ChunkManager) findOldestSessions(k int) []string {
|
||||
if k <= 0 || len(cm.sessionMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if k >= len(cm.sessionMap) {
|
||||
// Remove all sessions
|
||||
keys := make([]string, 0, len(cm.sessionMap))
|
||||
for key := range cm.sessionMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Collect all timestamps with keys
|
||||
type sessionAge struct {
|
||||
lastUsed time.Time
|
||||
key string
|
||||
}
|
||||
|
||||
sessions := make([]sessionAge, 0, len(cm.sessionMap))
|
||||
for key, entry := range cm.sessionMap {
|
||||
sessions = append(sessions, sessionAge{key: key, lastUsed: entry.LastUsed})
|
||||
}
|
||||
|
||||
// Partial sort: get the k smallest elements using selection
|
||||
// This is O(n*k) which is better than O(n log n) when k << n
|
||||
for i := 0; i < k; i++ {
|
||||
minIdx := i
|
||||
for j := i + 1; j < len(sessions); j++ {
|
||||
if sessions[j].lastUsed.Before(sessions[minIdx].lastUsed) {
|
||||
minIdx = j
|
||||
}
|
||||
}
|
||||
if minIdx != i {
|
||||
sessions[i], sessions[minIdx] = sessions[minIdx], sessions[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Return the k oldest
|
||||
result := make([]string, k)
|
||||
for i := 0; i < k; i++ {
|
||||
result[i] = sessions[i].key
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CanCreateSession checks if a new session can be created within limits
|
||||
func (cm *ChunkManager) CanCreateSession() (bool, error) {
|
||||
cm.mutex.RLock()
|
||||
currentCount := len(cm.sessionMap)
|
||||
cm.mutex.RUnlock()
|
||||
|
||||
// Hard limit check - never exceed maxSessions
|
||||
if currentCount >= cm.maxSessions {
|
||||
cm.logger.Error("Cannot create session: at maximum limit (%d)", cm.maxSessions)
|
||||
return false, fmt.Errorf("session storage at maximum capacity (%d sessions)", cm.maxSessions)
|
||||
}
|
||||
|
||||
// Emergency cleanup at 90% capacity
|
||||
emergencyThreshold := int(float64(cm.maxSessions) * 0.9)
|
||||
if currentCount >= emergencyThreshold {
|
||||
cm.logger.Info("Session storage at %d%% capacity, triggering emergency cleanup",
|
||||
(currentCount*100)/cm.maxSessions)
|
||||
cm.EmergencyCleanup()
|
||||
|
||||
// Recheck after cleanup
|
||||
cm.mutex.RLock()
|
||||
newCount := len(cm.sessionMap)
|
||||
cm.mutex.RUnlock()
|
||||
|
||||
if newCount >= cm.maxSessions {
|
||||
return false, fmt.Errorf("session storage full even after emergency cleanup (%d sessions)", newCount)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// EmergencyCleanup performs aggressive session cleanup when approaching limits
|
||||
func (cm *ChunkManager) EmergencyCleanup() {
|
||||
cm.mutex.Lock()
|
||||
defer cm.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
removed := 0
|
||||
|
||||
// Remove any expired sessions first
|
||||
expiredKeys := make([]string, 0)
|
||||
for key, entry := range cm.sessionMap {
|
||||
if now.After(entry.ExpiresAt) || now.Sub(entry.LastUsed) > cm.sessionTTL {
|
||||
expiredKeys = append(expiredKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range expiredKeys {
|
||||
if entry, exists := cm.sessionMap[key]; exists {
|
||||
atomic.AddInt64(&cm.bytesAllocated, -entry.SizeEstimate)
|
||||
}
|
||||
delete(cm.sessionMap, key)
|
||||
removed++
|
||||
}
|
||||
|
||||
// If still over 80% capacity, remove oldest sessions more aggressively
|
||||
targetCapacity := int(float64(cm.maxSessions) * 0.8)
|
||||
if len(cm.sessionMap) > targetCapacity {
|
||||
excessCount := len(cm.sessionMap) - targetCapacity
|
||||
|
||||
// Use efficient partial sort to find oldest sessions
|
||||
keysToRemove := cm.findOldestSessions(excessCount)
|
||||
|
||||
for _, key := range keysToRemove {
|
||||
if entry, exists := cm.sessionMap[key]; exists {
|
||||
atomic.AddInt64(&cm.bytesAllocated, -entry.SizeEstimate)
|
||||
}
|
||||
delete(cm.sessionMap, key)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
||||
cm.lastCleanup = now
|
||||
cm.logger.Infof("Emergency cleanup completed: removed %d sessions, %d remaining",
|
||||
removed, len(cm.sessionMap))
|
||||
|
||||
// Log memory stats after emergency cleanup (read only, no forced GC)
|
||||
cm.logger.Infof("Sessions after emergency cleanup: %d, Tracked bytes: %d",
|
||||
len(cm.sessionMap), atomic.LoadInt64(&cm.bytesAllocated))
|
||||
}
|
||||
|
||||
// GetSessionCount returns the current number of active sessions (for monitoring)
|
||||
func (cm *ChunkManager) GetSessionCount() int {
|
||||
cm.mutex.RLock()
|
||||
defer cm.mutex.RUnlock()
|
||||
return len(cm.sessionMap)
|
||||
}
|
||||
|
||||
// GetMemoryStats returns memory usage statistics for monitoring
|
||||
func (cm *ChunkManager) GetMemoryStats() map[string]interface{} {
|
||||
cm.mutex.RLock()
|
||||
sessionCount := len(cm.sessionMap)
|
||||
cm.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats["active_sessions"] = sessionCount
|
||||
stats["max_sessions"] = cm.maxSessions
|
||||
stats["bytes_allocated"] = atomic.LoadInt64(&cm.bytesAllocated)
|
||||
stats["peak_sessions"] = atomic.LoadInt64(&cm.peakSessions)
|
||||
stats["cleanup_count"] = atomic.LoadInt64(&cm.cleanupCount)
|
||||
stats["session_ttl_hours"] = cm.sessionTTL.Hours()
|
||||
|
||||
// Update peak sessions
|
||||
if int64(sessionCount) > atomic.LoadInt64(&cm.peakSessions) {
|
||||
atomic.StoreInt64(&cm.peakSessions, int64(sessionCount))
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,4 +14,4 @@ const devPluginVersion = "0.0.0-dev"
|
||||
// from build info at runtime (debug.ReadBuildInfo sees Traefik's build graph,
|
||||
// not the interpreted plugin). This build-stamped constant is therefore the
|
||||
// single source of truth for the version reported by anonymous usage telemetry.
|
||||
const traefikoidcPluginVersion = "0.0.0-dev"
|
||||
const traefikoidcPluginVersion = "1.0.63"
|
||||
|
||||
Reference in New Issue
Block a user