mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
Memory leaks hunting, part 1337.
This commit is contained in:
@@ -53,7 +53,7 @@ func NewCacheWithLogger(logger *Logger) *Cache {
|
|||||||
order: list.New(),
|
order: list.New(),
|
||||||
elems: make(map[string]*list.Element, DefaultMaxSize),
|
elems: make(map[string]*list.Element, DefaultMaxSize),
|
||||||
maxSize: DefaultMaxSize,
|
maxSize: DefaultMaxSize,
|
||||||
autoCleanupInterval: 5 * time.Minute,
|
autoCleanupInterval: 15 * time.Minute, // Increased from 5 minutes to reduce overhead
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
c.startAutoCleanup()
|
c.startAutoCleanup()
|
||||||
@@ -155,16 +155,21 @@ func (c *Cache) Cleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// evictOldest removes the least recently used (oldest) item from the cache.
|
// evictOldest removes the least recently used (oldest) item from the cache.
|
||||||
// It first attempts to find and remove an expired item from the front of the LRU list.
|
// It first attempts to find and remove expired items, checking up to 5 items
|
||||||
// If no expired items are found at the front, it removes the absolute oldest item (front of the list).
|
// from the front of the LRU list for efficiency. If no expired items are found,
|
||||||
|
// it removes the absolute oldest item (front of the list).
|
||||||
// This method is called internally by Set when the cache reaches its maximum size.
|
// This method is called internally by Set when the cache reaches its maximum size.
|
||||||
// Note: This function assumes the write lock is already held.
|
// Note: This function assumes the write lock is already held.
|
||||||
func (c *Cache) evictOldest() {
|
func (c *Cache) evictOldest() {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
elem := c.order.Front()
|
elem := c.order.Front()
|
||||||
|
|
||||||
// First try to find an expired item from the front
|
// Check up to 5 items from the front for expired entries
|
||||||
for elem != nil {
|
// This limits the search overhead while still finding expired items efficiently
|
||||||
|
const maxExpiredCheck = 5
|
||||||
|
checked := 0
|
||||||
|
|
||||||
|
for elem != nil && checked < maxExpiredCheck {
|
||||||
entry := elem.Value.(lruEntry)
|
entry := elem.Value.(lruEntry)
|
||||||
if item, exists := c.items[entry.key]; exists {
|
if item, exists := c.items[entry.key]; exists {
|
||||||
if now.After(item.ExpiresAt) {
|
if now.After(item.ExpiresAt) {
|
||||||
@@ -173,9 +178,10 @@ func (c *Cache) evictOldest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elem = elem.Next()
|
elem = elem.Next()
|
||||||
|
checked++
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no expired items found, remove the oldest item
|
// If no expired items found in the first few entries, remove the oldest item
|
||||||
if elem = c.order.Front(); elem != nil {
|
if elem = c.order.Front(); elem != nil {
|
||||||
entry := elem.Value.(lruEntry)
|
entry := elem.Value.(lruEntry)
|
||||||
c.removeItem(entry.key)
|
c.removeItem(entry.key)
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,646 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBackgroundGoroutineLeaks tests that background goroutines don't leak memory
|
||||||
|
// even when no requests are made to protected resources
|
||||||
|
func TestBackgroundGoroutineLeaks(t *testing.T) {
|
||||||
|
t.Run("Idle middleware memory growth", func(t *testing.T) {
|
||||||
|
// Force GC to get clean baseline
|
||||||
|
runtime.GC()
|
||||||
|
runtime.GC()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
baselineAlloc := m.Alloc
|
||||||
|
baselineGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
t.Logf("Baseline: Memory=%d KB, Goroutines=%d", baselineAlloc/1024, baselineGoroutines)
|
||||||
|
|
||||||
|
// Create middleware instance
|
||||||
|
config := CreateConfig()
|
||||||
|
config.ProviderURL = "https://example.com"
|
||||||
|
config.ClientID = "test-client"
|
||||||
|
config.ClientSecret = "test-secret"
|
||||||
|
config.SessionEncryptionKey = "test-encryption-key-that-is-long-enough-32bytes"
|
||||||
|
config.LogLevel = "error" // Reduce noise
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler, err := New(ctx, next, config, "test-middleware")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast to TraefikOidc to access internals
|
||||||
|
middleware, ok := handler.(*TraefikOidc)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Failed to cast to TraefikOidc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let it sit idle for a while - simulating no requests
|
||||||
|
// During this time, background goroutines are running
|
||||||
|
t.Log("Letting middleware sit idle for 30 seconds...")
|
||||||
|
|
||||||
|
// Take measurements every 5 seconds
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
currentAlloc := m.Alloc
|
||||||
|
currentGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||||
|
|
||||||
|
t.Logf("After %d seconds: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
|
(i+1)*5, allocIncrease, goroutineIncrease)
|
||||||
|
|
||||||
|
// Check for significant memory growth (more than 5MB)
|
||||||
|
if allocIncrease > 5.0 {
|
||||||
|
t.Errorf("Significant memory increase detected: %.2f MB after %d seconds of idle",
|
||||||
|
allocIncrease, (i+1)*5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for goroutine leaks (more than 10 extra goroutines)
|
||||||
|
if goroutineIncrease > 10 {
|
||||||
|
t.Errorf("Goroutine leak detected: %d extra goroutines after %d seconds",
|
||||||
|
goroutineIncrease, (i+1)*5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = middleware.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to close middleware: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for cleanup
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Final check
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
finalAlloc := m.Alloc
|
||||||
|
finalGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
finalGoroutineIncrease := finalGoroutines - baselineGoroutines
|
||||||
|
|
||||||
|
t.Logf("Final: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
|
finalAllocIncrease, finalGoroutineIncrease)
|
||||||
|
|
||||||
|
if finalGoroutineIncrease > 2 {
|
||||||
|
t.Errorf("Goroutines not cleaned up properly: %d extra goroutines remain",
|
||||||
|
finalGoroutineIncrease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientConnectionLeaks tests that HTTP clients don't leak connections
|
||||||
|
func TestHTTPClientConnectionLeaks(t *testing.T) {
|
||||||
|
t.Run("HTTP client connection accumulation", func(t *testing.T) {
|
||||||
|
// Create test server that simulates OIDC endpoints
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/.well-known/openid-configuration":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{
|
||||||
|
"issuer": "https://example.com",
|
||||||
|
"authorization_endpoint": "https://example.com/auth",
|
||||||
|
"token_endpoint": "https://example.com/token",
|
||||||
|
"jwks_uri": "https://example.com/jwks",
|
||||||
|
"userinfo_endpoint": "https://example.com/userinfo"
|
||||||
|
}`))
|
||||||
|
case "/jwks":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{"keys": []}`))
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Monitor connection count
|
||||||
|
getActiveConnections := func(client *http.Client) int {
|
||||||
|
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||||
|
// This is a simplified check - in reality we'd need to inspect
|
||||||
|
// the transport's connection pool
|
||||||
|
return transport.MaxIdleConnsPerHost
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multiple HTTP clients like the middleware does
|
||||||
|
clients := make([]*http.Client, 10)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
clients[i] = createDefaultHTTPClient()
|
||||||
|
|
||||||
|
// Make requests
|
||||||
|
resp, err := clients[i].Get(server.URL + "/.well-known/openid-configuration")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Check connection settings
|
||||||
|
conns := getActiveConnections(clients[i])
|
||||||
|
if conns > 1 {
|
||||||
|
t.Logf("Client %d has %d max idle connections per host", i, conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let connections sit idle (now only 5s with our optimizations)
|
||||||
|
time.Sleep(6 * time.Second) // Slightly longer than new IdleConnTimeout (5s)
|
||||||
|
|
||||||
|
// Force cleanup
|
||||||
|
for _, client := range clients {
|
||||||
|
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||||
|
transport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("HTTP clients cleaned up successfully")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCacheBackgroundTaskLeaks tests that cache background tasks don't leak
|
||||||
|
func TestCacheBackgroundTaskLeaks(t *testing.T) {
|
||||||
|
t.Run("Multiple cache instances with cleanup tasks", func(t *testing.T) {
|
||||||
|
initialGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
// Create many cache instances
|
||||||
|
caches := make([]*Cache, 50)
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
caches[i] = NewCache()
|
||||||
|
// Add some data
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
key := fmt.Sprintf("key-%d-%d", i, j)
|
||||||
|
caches[i].Set(key, "value", 5*time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all cleanup goroutines to start
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
afterCreateGoroutines := runtime.NumGoroutine()
|
||||||
|
goroutineIncrease := afterCreateGoroutines - initialGoroutines
|
||||||
|
|
||||||
|
t.Logf("Created %d caches, goroutine increase: %d", len(caches), goroutineIncrease)
|
||||||
|
|
||||||
|
// Expected: one cleanup goroutine per cache
|
||||||
|
if goroutineIncrease < len(caches) {
|
||||||
|
t.Errorf("Expected at least %d goroutines, got %d increase",
|
||||||
|
len(caches), goroutineIncrease)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all caches
|
||||||
|
for _, cache := range caches {
|
||||||
|
cache.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for goroutines to stop
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
finalGoroutines := runtime.NumGoroutine()
|
||||||
|
remainingGoroutines := finalGoroutines - initialGoroutines
|
||||||
|
|
||||||
|
if remainingGoroutines > 5 { // Allow small tolerance
|
||||||
|
t.Errorf("Cache cleanup goroutines not stopped properly: %d extra goroutines remain",
|
||||||
|
remainingGoroutines)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGlobalSingletonMemoryGrowth tests that global singletons don't grow unbounded
|
||||||
|
func TestGlobalSingletonMemoryGrowth(t *testing.T) {
|
||||||
|
t.Run("Global cache manager memory growth", func(t *testing.T) {
|
||||||
|
// Clean up any existing global state
|
||||||
|
CleanupGlobalCacheManager()
|
||||||
|
CleanupGlobalMemoryPools()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
baselineAlloc := m.Alloc
|
||||||
|
|
||||||
|
// Get global cache manager
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
cm := GetGlobalCacheManager(wg)
|
||||||
|
|
||||||
|
// Simulate continuous usage without cleanup
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
// Add to token cache
|
||||||
|
cm.GetSharedTokenCache().Set(
|
||||||
|
fmt.Sprintf("token-%d", i),
|
||||||
|
map[string]interface{}{"claim": fmt.Sprintf("value-%d", i)},
|
||||||
|
5*time.Minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to blacklist
|
||||||
|
cm.GetSharedTokenBlacklist().Set(
|
||||||
|
fmt.Sprintf("blacklist-%d", i),
|
||||||
|
true,
|
||||||
|
5*time.Minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Every 100 iterations, check memory
|
||||||
|
if i%100 == 0 {
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
currentAlloc := m.Alloc
|
||||||
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("After %d items: Memory increase=%.2f MB", i, allocIncrease)
|
||||||
|
|
||||||
|
// The caches should have max size limits
|
||||||
|
// If memory grows more than 10MB, there's likely a leak
|
||||||
|
if allocIncrease > 10.0 {
|
||||||
|
t.Errorf("Excessive memory growth in global caches: %.2f MB after %d items",
|
||||||
|
allocIncrease, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
CleanupGlobalCacheManager()
|
||||||
|
CleanupGlobalMemoryPools()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Force full cleanup of replay cache too
|
||||||
|
cleanupReplayCache()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
finalAlloc := m.Alloc
|
||||||
|
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("Final memory increase after cleanup: %.2f MB", finalAllocIncrease)
|
||||||
|
|
||||||
|
if finalAllocIncrease > 2.0 {
|
||||||
|
t.Errorf("Memory not properly released after cleanup: %.2f MB remains",
|
||||||
|
finalAllocIncrease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMetadataCacheRefreshLeak tests for memory leaks in metadata refresh
|
||||||
|
func TestMetadataCacheRefreshLeak(t *testing.T) {
|
||||||
|
t.Run("Metadata cache refresh memory leak", func(t *testing.T) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
cache := NewMetadataCacheWithLogger(wg, NewLogger("error"))
|
||||||
|
defer cache.Close()
|
||||||
|
|
||||||
|
// Mock HTTP client that returns metadata
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
// Return a large metadata response to amplify any leaks
|
||||||
|
w.Write([]byte(`{
|
||||||
|
"issuer": "https://example.com",
|
||||||
|
"authorization_endpoint": "https://example.com/auth",
|
||||||
|
"token_endpoint": "https://example.com/token",
|
||||||
|
"jwks_uri": "https://example.com/jwks",
|
||||||
|
"userinfo_endpoint": "https://example.com/userinfo",
|
||||||
|
"extra_field_1": "` + strings.Repeat("x", 1000) + `",
|
||||||
|
"extra_field_2": "` + strings.Repeat("y", 1000) + `"
|
||||||
|
}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
baselineAlloc := m.Alloc
|
||||||
|
|
||||||
|
// Simulate many metadata refreshes
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
_, err := cache.GetMetadata(server.URL, client, NewLogger("error"))
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Metadata fetch error (expected for test server): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force cache expiry to trigger refresh
|
||||||
|
cache.mutex.Lock()
|
||||||
|
cache.expiresAt = time.Now().Add(-1 * time.Hour)
|
||||||
|
cache.mutex.Unlock()
|
||||||
|
|
||||||
|
if i%20 == 0 && i > 0 {
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
currentAlloc := m.Alloc
|
||||||
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("After %d refreshes: Memory increase=%.2f MB", i, allocIncrease)
|
||||||
|
|
||||||
|
// Metadata cache should only store one copy
|
||||||
|
if allocIncrease > 3.0 {
|
||||||
|
t.Errorf("Metadata cache leak detected: %.2f MB after %d refreshes",
|
||||||
|
allocIncrease, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Close()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
finalAlloc := m.Alloc
|
||||||
|
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("Final memory after metadata cache closure: %.2f MB", finalAllocIncrease)
|
||||||
|
|
||||||
|
if finalAllocIncrease > 1.0 {
|
||||||
|
t.Errorf("Metadata cache not cleaned properly: %.2f MB remains", finalAllocIncrease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMemoryPoolLeak tests for leaks in memory pool management
|
||||||
|
func TestMemoryPoolLeak(t *testing.T) {
|
||||||
|
t.Run("Memory pool buffer leaks", func(t *testing.T) {
|
||||||
|
// Clean up any existing pools
|
||||||
|
CleanupGlobalMemoryPools()
|
||||||
|
|
||||||
|
pools := GetGlobalMemoryPools()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
baselineAlloc := m.Alloc
|
||||||
|
|
||||||
|
// Simulate heavy buffer usage
|
||||||
|
var buffers [][]byte
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
buf := pools.GetHTTPResponseBuffer()
|
||||||
|
// Simulate using the buffer
|
||||||
|
copy(buf, []byte("test data"))
|
||||||
|
|
||||||
|
// 90% of the time, return the buffer
|
||||||
|
// 10% of the time, "forget" to return it (simulating a leak)
|
||||||
|
if i%10 != 0 {
|
||||||
|
pools.PutHTTPResponseBuffer(buf)
|
||||||
|
} else {
|
||||||
|
// Keep reference to simulate leak
|
||||||
|
buffers = append(buffers, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i%100 == 0 && i > 0 {
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
currentAlloc := m.Alloc
|
||||||
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("After %d buffer operations: Memory increase=%.2f MB, Leaked buffers=%d",
|
||||||
|
i, allocIncrease, len(buffers))
|
||||||
|
|
||||||
|
// With proper pooling, memory should be bounded
|
||||||
|
if allocIncrease > 5.0 {
|
||||||
|
t.Errorf("Memory pool leak detected: %.2f MB after %d operations",
|
||||||
|
allocIncrease, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the "leaked" buffers
|
||||||
|
for _, buf := range buffers {
|
||||||
|
pools.PutHTTPResponseBuffer(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanupGlobalMemoryPools()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
finalAlloc := m.Alloc
|
||||||
|
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
|
||||||
|
t.Logf("Final memory after pool cleanup: %.2f MB", finalAllocIncrease)
|
||||||
|
|
||||||
|
if finalAllocIncrease > 1.0 {
|
||||||
|
t.Errorf("Memory pools not cleaned properly: %.2f MB remains", finalAllocIncrease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentMemoryLeaks tests for memory leaks under concurrent load
|
||||||
|
func TestConcurrentMemoryLeaks(t *testing.T) {
|
||||||
|
t.Run("Concurrent operations memory stability", func(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
resetGlobalState()
|
||||||
|
|
||||||
|
// Set lower GC percentage to trigger GC more frequently
|
||||||
|
debug.SetGCPercent(50)
|
||||||
|
defer debug.SetGCPercent(100)
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
baselineAlloc := m.Alloc
|
||||||
|
baselineGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
// Create a mock OIDC server
|
||||||
|
var mockServerURL string
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/.well-known/openid-configuration":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"issuer": mockServerURL,
|
||||||
|
"authorization_endpoint": mockServerURL + "/auth",
|
||||||
|
"token_endpoint": mockServerURL + "/token",
|
||||||
|
"userinfo_endpoint": mockServerURL + "/userinfo",
|
||||||
|
"jwks_uri": mockServerURL + "/keys",
|
||||||
|
})
|
||||||
|
case "/keys":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"keys": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"kty": "RSA",
|
||||||
|
"use": "sig",
|
||||||
|
"kid": "test-key",
|
||||||
|
"n": "test-modulus",
|
||||||
|
"e": "AQAB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer mockServer.Close()
|
||||||
|
mockServerURL = mockServer.URL
|
||||||
|
|
||||||
|
// Create middleware config with mock server
|
||||||
|
config := createTestConfig()
|
||||||
|
config.ProviderURL = mockServerURL
|
||||||
|
config.LogLevel = "error"
|
||||||
|
|
||||||
|
// Create middleware directly without using New() to avoid automatic metadata fetch
|
||||||
|
middleware := &TraefikOidc{
|
||||||
|
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }),
|
||||||
|
providerURL: config.ProviderURL,
|
||||||
|
clientID: config.ClientID,
|
||||||
|
clientSecret: config.ClientSecret,
|
||||||
|
redirURLPath: "/callback",
|
||||||
|
scopes: []string{"openid", "email", "profile"},
|
||||||
|
logger: NewLogger(config.LogLevel),
|
||||||
|
excludedURLs: make(map[string]struct{}),
|
||||||
|
httpClient: &http.Client{Timeout: 5 * time.Second},
|
||||||
|
sessionManager: nil, // Will be created below
|
||||||
|
tokenCache: NewTokenCache(),
|
||||||
|
tokenBlacklist: NewCache(),
|
||||||
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
|
goroutineWG: &sync.WaitGroup{},
|
||||||
|
firstRequestMutex: sync.Mutex{},
|
||||||
|
firstRequestReceived: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session manager
|
||||||
|
var err error
|
||||||
|
middleware.sessionManager, err = NewSessionManager(
|
||||||
|
config.SessionEncryptionKey,
|
||||||
|
config.ForceHTTPS,
|
||||||
|
config.CookieDomain,
|
||||||
|
middleware.logger,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate concurrent operations
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
stopChan := make(chan struct{})
|
||||||
|
|
||||||
|
// Worker that continuously performs operations
|
||||||
|
worker := func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Simulate various operations
|
||||||
|
switch id % 4 {
|
||||||
|
case 0:
|
||||||
|
// Cache operations
|
||||||
|
cache := NewCache()
|
||||||
|
cache.Set(fmt.Sprintf("key-%d", id), "value", time.Minute)
|
||||||
|
cache.Get(fmt.Sprintf("key-%d", id))
|
||||||
|
cache.Close()
|
||||||
|
case 1:
|
||||||
|
// Session operations
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
session, _ := middleware.sessionManager.GetSession(req)
|
||||||
|
if session != nil {
|
||||||
|
session.SetAccessToken("token")
|
||||||
|
session.returnToPoolSafely()
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
// Token cache operations
|
||||||
|
cm := GetGlobalCacheManager(nil)
|
||||||
|
if cm != nil {
|
||||||
|
tc := cm.GetSharedTokenCache()
|
||||||
|
tc.Set("test-token", map[string]interface{}{"test": "data"}, time.Minute)
|
||||||
|
tc.Get("test-token")
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
// Memory pool operations
|
||||||
|
pools := GetGlobalMemoryPools()
|
||||||
|
buf := pools.GetHTTPResponseBuffer()
|
||||||
|
pools.PutHTTPResponseBuffer(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent tight loop
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start workers
|
||||||
|
numWorkers := 20
|
||||||
|
wg.Add(numWorkers)
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
go worker(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let it run and measure periodically
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
currentAlloc := m.Alloc
|
||||||
|
currentGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||||
|
|
||||||
|
t.Logf("After %d seconds of concurrent load: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
|
(i+1)*5, allocIncrease, goroutineIncrease)
|
||||||
|
|
||||||
|
// Under sustained concurrent load, memory should stabilize
|
||||||
|
if i > 2 && allocIncrease > 20.0 {
|
||||||
|
t.Errorf("Memory leak under concurrent load: %.2f MB after %d seconds",
|
||||||
|
allocIncrease, (i+1)*5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop workers
|
||||||
|
close(stopChan)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
middleware.Close()
|
||||||
|
|
||||||
|
// Final measurements
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
finalAlloc := m.Alloc
|
||||||
|
finalGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||||
|
finalGoroutineIncrease := finalGoroutines - baselineGoroutines
|
||||||
|
|
||||||
|
t.Logf("Final after cleanup: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
|
finalAllocIncrease, finalGoroutineIncrease)
|
||||||
|
|
||||||
|
if finalGoroutineIncrease > 5 {
|
||||||
|
t.Errorf("Goroutines not cleaned up after concurrent operations: %d extra remain",
|
||||||
|
finalGoroutineIncrease)
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalAllocIncrease > 5.0 {
|
||||||
|
t.Errorf("Memory not released after concurrent operations: %.2f MB remains",
|
||||||
|
finalAllocIncrease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -42,17 +42,17 @@ func createDefaultHTTPClient() *http.Client {
|
|||||||
return dialer.DialContext(ctx, network, addr)
|
return dialer.DialContext(ctx, network, addr)
|
||||||
},
|
},
|
||||||
ForceAttemptHTTP2: true,
|
ForceAttemptHTTP2: true,
|
||||||
TLSHandshakeTimeout: 2 * time.Second, // Reduced TLS handshake timeout
|
TLSHandshakeTimeout: 2 * time.Second, // Reduced TLS handshake timeout
|
||||||
ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
|
ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
|
||||||
MaxIdleConns: 5, // Further reduced to prevent connection accumulation
|
MaxIdleConns: 2, // Reduced from 5 to minimize idle connections
|
||||||
MaxIdleConnsPerHost: 1, // Further reduced to limit per-host connections
|
MaxIdleConnsPerHost: 1, // Keep minimal idle connections per host
|
||||||
IdleConnTimeout: 15 * time.Second, // Further reduced to close idle connections faster
|
IdleConnTimeout: 5 * time.Second, // Reduced from 15s to close idle connections faster
|
||||||
DisableKeepAlives: false, // Enable connection reuse
|
DisableKeepAlives: false, // Enable connection reuse
|
||||||
MaxConnsPerHost: 5, // Further reduced to limit concurrent connections
|
MaxConnsPerHost: 2, // Reduced from 5 to limit concurrent connections
|
||||||
ResponseHeaderTimeout: 3 * time.Second, // Reduced timeout for reading response headers
|
ResponseHeaderTimeout: 3 * time.Second, // Reduced timeout for reading response headers
|
||||||
DisableCompression: false, // Enable compression for bandwidth efficiency
|
DisableCompression: false, // Enable compression for bandwidth efficiency
|
||||||
WriteBufferSize: 4096, // Write buffer size for connections
|
WriteBufferSize: 4096, // Write buffer size for connections
|
||||||
ReadBufferSize: 4096, // Read buffer size for connections
|
ReadBufferSize: 4096, // Read buffer size for connections
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
@@ -290,6 +290,10 @@ type TraefikOidc struct {
|
|||||||
enablePKCE bool
|
enablePKCE bool
|
||||||
overrideScopes bool
|
overrideScopes bool
|
||||||
suppressDiagnosticLogs bool
|
suppressDiagnosticLogs bool
|
||||||
|
firstRequestReceived bool
|
||||||
|
firstRequestMutex sync.Mutex
|
||||||
|
metadataRefreshStarted bool
|
||||||
|
providerURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderMetadata represents the OpenID Connect provider's discovery metadata.
|
// ProviderMetadata represents the OpenID Connect provider's discovery metadata.
|
||||||
@@ -773,7 +777,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
|
|
||||||
t.tokenVerifier = t
|
t.tokenVerifier = t
|
||||||
t.jwtVerifier = t
|
t.jwtVerifier = t
|
||||||
t.startTokenCleanup()
|
// Delay startTokenCleanup() until first request
|
||||||
t.tokenExchanger = t // Initialize the interface field to self
|
t.tokenExchanger = t // Initialize the interface field to self
|
||||||
|
|
||||||
// Initialize and parse header templates with safe field access
|
// Initialize and parse header templates with safe field access
|
||||||
@@ -817,6 +821,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
|
|
||||||
startReplayCacheCleanup(pluginCtx, logger)
|
startReplayCacheCleanup(pluginCtx, logger)
|
||||||
logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes)
|
logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes)
|
||||||
|
|
||||||
|
// Store provider URL for later use
|
||||||
|
t.providerURL = config.ProviderURL
|
||||||
go t.initializeMetadata(config.ProviderURL)
|
go t.initializeMetadata(config.ProviderURL)
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
@@ -853,8 +860,8 @@ func (t *TraefikOidc) initializeMetadata(providerURL string) {
|
|||||||
t.logger.Debug("Successfully initialized provider metadata")
|
t.logger.Debug("Successfully initialized provider metadata")
|
||||||
t.updateMetadataEndpoints(metadata)
|
t.updateMetadataEndpoints(metadata)
|
||||||
|
|
||||||
// Start metadata refresh goroutine
|
// Delay metadata refresh goroutine until first request
|
||||||
go t.startMetadataRefresh(providerURL)
|
// It will be started in ServeHTTP when needed
|
||||||
|
|
||||||
// Only close channel on success
|
// Only close channel on success
|
||||||
close(t.initComplete)
|
close(t.initComplete)
|
||||||
@@ -890,11 +897,19 @@ func (t *TraefikOidc) updateMetadataEndpoints(metadata *ProviderMetadata) {
|
|||||||
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
||||||
// Use longer interval to reduce memory pressure from refresh attempts
|
// Use longer interval to reduce memory pressure from refresh attempts
|
||||||
ticker := time.NewTicker(2 * time.Hour) // Increased from 1 hour
|
ticker := time.NewTicker(2 * time.Hour) // Increased from 1 hour
|
||||||
t.goroutineWG.Add(1) // Track this goroutine
|
|
||||||
|
// Check if WaitGroup is initialized (it might be nil in tests)
|
||||||
|
if t.goroutineWG != nil {
|
||||||
|
t.goroutineWG.Add(1) // Track this goroutine
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer t.goroutineWG.Done() // Signal completion when goroutine exits
|
defer func() {
|
||||||
defer ticker.Stop() // Ensure ticker is always stopped
|
if t.goroutineWG != nil {
|
||||||
|
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer ticker.Stop() // Ensure ticker is always stopped
|
||||||
|
|
||||||
consecutiveFailures := 0
|
consecutiveFailures := 0
|
||||||
for {
|
for {
|
||||||
@@ -1058,6 +1073,23 @@ func fetchMetadata(wellKnownURL string, httpClient *http.Client) (*ProviderMetad
|
|||||||
// - Authentication state management
|
// - Authentication state management
|
||||||
// - Header injection for authenticated requests
|
// - Header injection for authenticated requests
|
||||||
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Start background tasks on first request (except for health checks)
|
||||||
|
if !t.firstRequestReceived && !strings.HasPrefix(req.URL.Path, "/health") {
|
||||||
|
t.firstRequestMutex.Lock()
|
||||||
|
if !t.firstRequestReceived {
|
||||||
|
t.firstRequestReceived = true
|
||||||
|
t.logger.Debug("Starting background tasks on first request")
|
||||||
|
t.startTokenCleanup()
|
||||||
|
|
||||||
|
// Also start metadata refresh if not already started
|
||||||
|
if !t.metadataRefreshStarted && t.providerURL != "" {
|
||||||
|
t.metadataRefreshStarted = true
|
||||||
|
go t.startMetadataRefresh(t.providerURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.firstRequestMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for provider metadata initialization to complete
|
// Wait for provider metadata initialization to complete
|
||||||
select {
|
select {
|
||||||
case <-t.initComplete:
|
case <-t.initComplete:
|
||||||
@@ -2050,11 +2082,17 @@ func (t *TraefikOidc) validateHost(host string) error {
|
|||||||
// panic recovery to ensure stability.
|
// panic recovery to ensure stability.
|
||||||
func (t *TraefikOidc) startTokenCleanup() {
|
func (t *TraefikOidc) startTokenCleanup() {
|
||||||
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
||||||
t.goroutineWG.Add(1) // Track this goroutine
|
|
||||||
|
// Check if WaitGroup is initialized (it might be nil in tests)
|
||||||
|
if t.goroutineWG != nil {
|
||||||
|
t.goroutineWG.Add(1) // Track this goroutine
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
t.goroutineWG.Done() // Signal completion when goroutine exits
|
if t.goroutineWG != nil {
|
||||||
ticker.Stop() // Ensure ticker is always stopped
|
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||||
|
}
|
||||||
|
ticker.Stop() // Ensure ticker is always stopped
|
||||||
|
|
||||||
// CRITICAL: Recover from panics to prevent middleware crashes
|
// CRITICAL: Recover from panics to prevent middleware crashes
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -2840,17 +2878,22 @@ func (t *TraefikOidc) Close() error {
|
|||||||
t.logger.Debug("metadataRefreshStopChan closed")
|
t.logger.Debug("metadataRefreshStopChan closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan struct{})
|
// Only wait for goroutines if WaitGroup is initialized
|
||||||
go func() {
|
if t.goroutineWG != nil {
|
||||||
t.goroutineWG.Wait()
|
done := make(chan struct{})
|
||||||
close(done)
|
go func() {
|
||||||
}()
|
t.goroutineWG.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
t.logger.Debug("All background goroutines stopped gracefully")
|
t.logger.Debug("All background goroutines stopped gracefully")
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(10 * time.Second):
|
||||||
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.logger.Debug("No goroutineWG to wait for (likely in test)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.httpClient != nil {
|
if t.httpClient != nil {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LazyBackgroundTask wraps BackgroundTask to start only when needed
|
||||||
|
type LazyBackgroundTask struct {
|
||||||
|
*BackgroundTask
|
||||||
|
started bool
|
||||||
|
startOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLazyBackgroundTask creates a background task that doesn't start immediately
|
||||||
|
func NewLazyBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger, wg ...*sync.WaitGroup) *LazyBackgroundTask {
|
||||||
|
return &LazyBackgroundTask{
|
||||||
|
BackgroundTask: NewBackgroundTask(name, interval, taskFunc, logger, wg...),
|
||||||
|
started: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartIfNeeded starts the task only if it hasn't been started yet
|
||||||
|
func (lt *LazyBackgroundTask) StartIfNeeded() {
|
||||||
|
lt.startOnce.Do(func() {
|
||||||
|
if !lt.started {
|
||||||
|
lt.BackgroundTask.Start()
|
||||||
|
lt.started = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the task if it was started
|
||||||
|
func (lt *LazyBackgroundTask) Stop() {
|
||||||
|
if lt.started {
|
||||||
|
lt.BackgroundTask.Stop()
|
||||||
|
lt.started = false
|
||||||
|
lt.startOnce = sync.Once{} // Reset for potential restart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLazyCacheWithLogger creates a cache that doesn't start cleanup until first use
|
||||||
|
func NewLazyCacheWithLogger(logger *Logger) *Cache {
|
||||||
|
if logger == nil {
|
||||||
|
logger = newNoOpLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Cache{
|
||||||
|
items: make(map[string]CacheItem, DefaultMaxSize),
|
||||||
|
order: list.New(),
|
||||||
|
elems: make(map[string]*list.Element, DefaultMaxSize),
|
||||||
|
maxSize: DefaultMaxSize,
|
||||||
|
autoCleanupInterval: 10 * time.Minute, // Increased from 5 minutes
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
// Don't start cleanup immediately - it will be started on first use
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLazyCache creates a cache that doesn't start cleanup immediately
|
||||||
|
func NewLazyCache() *Cache {
|
||||||
|
return NewLazyCacheWithLogger(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupIdleConnections periodically closes idle HTTP connections
|
||||||
|
func CleanupIdleConnections(client *http.Client, interval time.Duration, stopChan <-chan struct{}) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||||
|
transport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
// Final cleanup
|
||||||
|
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||||
|
transport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptimizedMiddlewareConfig provides configuration for memory-optimized middleware
|
||||||
|
type OptimizedMiddlewareConfig struct {
|
||||||
|
// DelayBackgroundTasks delays starting background tasks until first request
|
||||||
|
DelayBackgroundTasks bool
|
||||||
|
|
||||||
|
// ReducedCleanupIntervals uses longer intervals for cleanup tasks
|
||||||
|
ReducedCleanupIntervals bool
|
||||||
|
|
||||||
|
// AggressiveConnectionCleanup closes idle connections more aggressively
|
||||||
|
AggressiveConnectionCleanup bool
|
||||||
|
|
||||||
|
// MinimalCacheSize uses smaller default cache sizes
|
||||||
|
MinimalCacheSize bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptimizedConfig returns a configuration optimized for low memory usage
|
||||||
|
func DefaultOptimizedConfig() *OptimizedMiddlewareConfig {
|
||||||
|
return &OptimizedMiddlewareConfig{
|
||||||
|
DelayBackgroundTasks: true,
|
||||||
|
ReducedCleanupIntervals: true,
|
||||||
|
AggressiveConnectionCleanup: true,
|
||||||
|
MinimalCacheSize: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -36,7 +36,7 @@ func NewMetadataCacheWithLogger(wg *sync.WaitGroup, logger *Logger) *MetadataCac
|
|||||||
}
|
}
|
||||||
|
|
||||||
c := &MetadataCache{
|
c := &MetadataCache{
|
||||||
autoCleanupInterval: 5 * time.Minute,
|
autoCleanupInterval: 30 * time.Minute, // Increased from 5 minutes since metadata changes rarely
|
||||||
logger: logger,
|
logger: logger,
|
||||||
wg: wg,
|
wg: wg,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ func TestRefreshGracePeriodConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenRefreshWithinGracePeriod(t *testing.T) {
|
func TestTokenRefreshWithinGracePeriod(t *testing.T) {
|
||||||
|
// Reset global state to prevent test interference
|
||||||
|
resetGlobalState()
|
||||||
|
|
||||||
refreshCount := int32(0)
|
refreshCount := int32(0)
|
||||||
tokenVersion := int32(1)
|
tokenVersion := int32(1)
|
||||||
|
|
||||||
@@ -286,6 +289,9 @@ func TestGracePeriodWithProviderSpecificBehavior(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRefreshGracePeriodConcurrency(t *testing.T) {
|
func TestRefreshGracePeriodConcurrency(t *testing.T) {
|
||||||
|
// Reset global state to prevent test interference
|
||||||
|
resetGlobalState()
|
||||||
|
|
||||||
var refreshMutex sync.Mutex
|
var refreshMutex sync.Mutex
|
||||||
refreshCount := 0
|
refreshCount := 0
|
||||||
blockedRequests := int32(0)
|
blockedRequests := int32(0)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a fresh instance for this test
|
// Create a fresh instance for this test
|
||||||
|
var goroutineWG sync.WaitGroup
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
@@ -51,6 +52,7 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
|||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{},
|
||||||
extractClaimsFunc: extractClaims,
|
extractClaimsFunc: extractClaims,
|
||||||
|
goroutineWG: &goroutineWG,
|
||||||
}
|
}
|
||||||
tOidc.tokenVerifier = tOidc
|
tOidc.tokenVerifier = tOidc
|
||||||
tOidc.jwtVerifier = tOidc
|
tOidc.jwtVerifier = tOidc
|
||||||
@@ -497,6 +499,7 @@ func TestMaliciousInputValidation(t *testing.T) {
|
|||||||
for _, test := range maliciousInputs {
|
for _, test := range maliciousInputs {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
// Create a fresh instance for each test to avoid rate limiting issues
|
// Create a fresh instance for each test to avoid rate limiting issues
|
||||||
|
var goroutineWG sync.WaitGroup
|
||||||
freshOidc := &TraefikOidc{
|
freshOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
@@ -508,6 +511,7 @@ func TestMaliciousInputValidation(t *testing.T) {
|
|||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{},
|
||||||
extractClaimsFunc: extractClaims,
|
extractClaimsFunc: extractClaims,
|
||||||
|
goroutineWG: &goroutineWG,
|
||||||
}
|
}
|
||||||
freshOidc.tokenVerifier = freshOidc
|
freshOidc.tokenVerifier = freshOidc
|
||||||
freshOidc.jwtVerifier = freshOidc
|
freshOidc.jwtVerifier = freshOidc
|
||||||
@@ -731,6 +735,7 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create fresh instance with high rate limit
|
// Create fresh instance with high rate limit
|
||||||
|
var goroutineWG sync.WaitGroup
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
@@ -742,6 +747,7 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
|||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{},
|
||||||
extractClaimsFunc: extractClaims,
|
extractClaimsFunc: extractClaims,
|
||||||
|
goroutineWG: &goroutineWG,
|
||||||
}
|
}
|
||||||
tOidc.tokenVerifier = tOidc
|
tOidc.tokenVerifier = tOidc
|
||||||
tOidc.jwtVerifier = tOidc
|
tOidc.jwtVerifier = tOidc
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,6 +26,33 @@ func (w *testWriter) Write(p []byte) (n int, err error) {
|
|||||||
|
|
||||||
// Test helper adapters for the new test files
|
// Test helper adapters for the new test files
|
||||||
|
|
||||||
|
// resetGlobalState resets all global singletons to prevent test interference
|
||||||
|
func resetGlobalState() {
|
||||||
|
// Reset global cache manager
|
||||||
|
cacheManagerMutex.Lock()
|
||||||
|
if globalCacheManager != nil {
|
||||||
|
globalCacheManager.Close()
|
||||||
|
globalCacheManager = nil
|
||||||
|
}
|
||||||
|
cacheManagerOnce = sync.Once{}
|
||||||
|
cacheManagerMutex.Unlock()
|
||||||
|
|
||||||
|
// Reset replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
if replayCache != nil {
|
||||||
|
replayCache.Close()
|
||||||
|
replayCache = nil
|
||||||
|
}
|
||||||
|
replayCacheOnce = sync.Once{}
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Reset memory pools
|
||||||
|
memoryPoolMutex.Lock()
|
||||||
|
globalMemoryPools = nil
|
||||||
|
memoryPoolOnce = sync.Once{}
|
||||||
|
memoryPoolMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// createTestConfig creates a config with all required fields populated for testing
|
// createTestConfig creates a config with all required fields populated for testing
|
||||||
func createTestConfig() *Config {
|
func createTestConfig() *Config {
|
||||||
config := CreateConfig()
|
config := CreateConfig()
|
||||||
@@ -38,6 +66,9 @@ func createTestConfig() *Config {
|
|||||||
|
|
||||||
// setupTestOIDCMiddleware creates a test OIDC middleware instance with mock servers
|
// setupTestOIDCMiddleware creates a test OIDC middleware instance with mock servers
|
||||||
func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httptest.Server) {
|
func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httptest.Server) {
|
||||||
|
// Reset global state to ensure test isolation
|
||||||
|
resetGlobalState()
|
||||||
|
|
||||||
// Create mock OIDC server
|
// Create mock OIDC server
|
||||||
var serverURL string
|
var serverURL string
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -114,6 +145,9 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt
|
|||||||
testTokenURL := testIssuerURL + "/token"
|
testTokenURL := testIssuerURL + "/token"
|
||||||
testJWKSURL := testIssuerURL + "/keys"
|
testJWKSURL := testIssuerURL + "/keys"
|
||||||
|
|
||||||
|
// Create WaitGroup for background goroutines
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Create TraefikOidc instance directly
|
// Create TraefikOidc instance directly
|
||||||
oidc := &TraefikOidc{
|
oidc := &TraefikOidc{
|
||||||
next: nextHandler,
|
next: nextHandler,
|
||||||
@@ -145,6 +179,8 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt
|
|||||||
jwkCache: &JWKCache{},
|
jwkCache: &JWKCache{},
|
||||||
metadataCache: NewMetadataCache(nil),
|
metadataCache: NewMetadataCache(nil),
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
goroutineWG: &wg,
|
||||||
|
providerURL: serverURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process excluded URLs
|
// Process excluded URLs
|
||||||
|
|||||||
Reference in New Issue
Block a user