mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
665 lines
19 KiB
Go
665 lines
19 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"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 test cleanup
|
|
tc := newTestCleanup(t)
|
|
|
|
// Create middleware instance with mock setup
|
|
config := createTestConfig()
|
|
config.LogLevel = "error" // Reduce noise
|
|
|
|
middleware, server := setupTestOIDCMiddleware(t, config)
|
|
tc.addServer(server)
|
|
tc.addOIDC(middleware)
|
|
|
|
// Let it sit idle for a while - simulating no requests
|
|
// During this time, background goroutines are running
|
|
t.Log("Letting middleware sit idle for 3 seconds...")
|
|
|
|
// Take measurements every 1 second
|
|
for i := 0; i < 3; i++ {
|
|
time.Sleep(1 * 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), 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))
|
|
}
|
|
|
|
// 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
|
|
if err := middleware.Close(); 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 briefly to test cleanup
|
|
time.Sleep(1 * time.Second) // Reduced for faster tests
|
|
|
|
// 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) {
|
|
// Reset global state to prevent test interference
|
|
resetGlobalState()
|
|
|
|
initialGoroutines := runtime.NumGoroutine()
|
|
|
|
// Create many cache instances
|
|
caches := make([]*Cache, 50)
|
|
|
|
// Ensure cleanup even on test failure or panic
|
|
defer func() {
|
|
for _, cache := range caches {
|
|
if cache != nil {
|
|
cache.Close()
|
|
}
|
|
}
|
|
// Wait a bit for goroutines to stop
|
|
time.Sleep(100 * time.Millisecond)
|
|
}()
|
|
|
|
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 with timeout
|
|
done := make(chan bool)
|
|
go func() {
|
|
for i := 0; i < 20; i++ { // Try for 2 seconds
|
|
time.Sleep(100 * time.Millisecond)
|
|
currentGoroutines := runtime.NumGoroutine()
|
|
if currentGoroutines-initialGoroutines <= 5 {
|
|
done <- true
|
|
return
|
|
}
|
|
}
|
|
done <- false
|
|
}()
|
|
|
|
success := <-done
|
|
finalGoroutines := runtime.NumGoroutine()
|
|
remainingGoroutines := finalGoroutines - initialGoroutines
|
|
|
|
if !success && 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
|
|
|
|
// Handle potential negative differences (memory freed beyond baseline)
|
|
var finalAllocIncrease float64
|
|
if finalAlloc >= baselineAlloc {
|
|
finalAllocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
|
} else {
|
|
// Memory decreased below baseline - this is good!
|
|
finalAllocIncrease = -float64(baselineAlloc-finalAlloc) / 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 < 3; i++ {
|
|
time.Sleep(1 * 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), allocIncrease, goroutineIncrease)
|
|
|
|
// Under sustained concurrent load, memory should stabilize
|
|
if i > 1 && allocIncrease > 20.0 {
|
|
t.Errorf("Memory leak under concurrent load: %.2f MB after %d seconds",
|
|
allocIncrease, (i + 1))
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|