Files
traefikoidc/connectivity_issues_test.go
T

1317 lines
38 KiB
Go

package traefikoidc
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/traefikoidc/circuit_breaker"
)
// TestConnectionPoolingFailureScenarios tests connection pooling behavior during various failure scenarios
func TestConnectionPoolingFailureScenarios(t *testing.T) {
if testing.Short() {
t.Skip("Skipping connection pooling failure tests in short mode")
}
t.Run("pool_maintains_connections_during_intermittent_failures", func(t *testing.T) {
// Create a server that fails 50% of requests
failureCount := int64(0)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.AddInt64(&failureCount, 1)%2 == 0 {
// Fail every other request
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
pool := GetGlobalTransportPool()
defer pool.Cleanup()
config := DefaultHTTPClientConfig()
client := CreatePooledHTTPClient(config)
// Make multiple concurrent requests to test pool behavior
var wg sync.WaitGroup
successCount := int64(0)
errorCount := int64(0)
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := client.Get(server.URL)
if err != nil || resp.StatusCode != http.StatusOK {
atomic.AddInt64(&errorCount, 1)
} else {
atomic.AddInt64(&successCount, 1)
resp.Body.Close()
}
}()
}
wg.Wait()
// Verify that both successes and errors occurred
finalSuccess := atomic.LoadInt64(&successCount)
finalError := atomic.LoadInt64(&errorCount)
assert.Greater(t, finalSuccess, int64(0), "Should have some successful requests")
assert.Greater(t, finalError, int64(0), "Should have some failed requests")
// Verify connection pool reuse by checking transport reuse
pool.mu.RLock()
transportCount := len(pool.transports)
pool.mu.RUnlock()
assert.Equal(t, 1, transportCount, "Should reuse a single transport for same config")
})
t.Run("pool_recovers_from_complete_outages", func(t *testing.T) {
// Server that starts failing, then recovers
serverDown := int64(1) // Start with server down
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt64(&serverDown) == 1 {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
pool := GetGlobalTransportPool()
defer pool.Cleanup()
config := DefaultHTTPClientConfig()
client := CreatePooledHTTPClient(config)
// Make requests while server is down
downRequests := 5
for i := 0; i < downRequests; i++ {
resp, err := client.Get(server.URL)
if err == nil {
assert.NotEqual(t, http.StatusOK, resp.StatusCode, "Server should be down")
resp.Body.Close()
}
}
// Bring server back up
atomic.StoreInt64(&serverDown, 0)
// Verify recovery
upRequests := 5
successCount := 0
for i := 0; i < upRequests; i++ {
resp, err := client.Get(server.URL)
require.NoError(t, err, "Should be able to make requests after recovery")
if resp.StatusCode == http.StatusOK {
successCount++
}
resp.Body.Close()
}
assert.Equal(t, upRequests, successCount, "All requests should succeed after recovery")
// Verify transport pool still functional
pool.mu.RLock()
transportCount := len(pool.transports)
var refCount int
for _, shared := range pool.transports {
refCount += shared.refCount
}
pool.mu.RUnlock()
assert.Equal(t, 1, transportCount, "Should maintain single transport")
assert.Greater(t, refCount, 0, "Transport should still be referenced")
})
t.Run("pool_handles_connection_timeout_gracefully", func(t *testing.T) {
// Server that delays responses to cause timeouts
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond) // Longer than client timeout
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
pool := GetGlobalTransportPool()
defer pool.Cleanup()
config := DefaultHTTPClientConfig()
config.Timeout = 50 * time.Millisecond // Short timeout
client := CreatePooledHTTPClient(config)
// Make requests that will timeout
var wg sync.WaitGroup
timeoutCount := int64(0)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := client.Get(server.URL)
if err != nil {
atomic.AddInt64(&timeoutCount, 1)
}
}()
}
wg.Wait()
assert.Greater(t, atomic.LoadInt64(&timeoutCount), int64(0), "Should have timeout errors")
// Verify transport pool is still functional after timeouts
pool.mu.RLock()
transportCount := len(pool.transports)
pool.mu.RUnlock()
assert.Equal(t, 1, transportCount, "Transport should still exist after timeouts")
})
t.Run("pool_enforces_connection_limits_during_load", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Millisecond) // Simulate processing time
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
pool := GetGlobalTransportPool()
defer pool.Cleanup()
config := DefaultHTTPClientConfig()
client := CreatePooledHTTPClient(config)
// Create high load to test connection limits
var wg sync.WaitGroup
requestCount := 50
completedRequests := int64(0)
for i := 0; i < requestCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := client.Get(server.URL)
if err == nil {
atomic.AddInt64(&completedRequests, 1)
resp.Body.Close()
}
}()
}
wg.Wait()
completed := atomic.LoadInt64(&completedRequests)
assert.Greater(t, completed, int64(0), "Should complete some requests")
// Verify connection limits are enforced
pool.mu.RLock()
for _, shared := range pool.transports {
// Check that transport limits are reasonable
transport := shared.transport
assert.LessOrEqual(t, transport.MaxIdleConnsPerHost, 5, "MaxIdleConnsPerHost should be limited")
assert.LessOrEqual(t, transport.MaxConnsPerHost, 10, "MaxConnsPerHost should be limited")
}
pool.mu.RUnlock()
})
}
// TestCircuitBreakerTokenOperations tests circuit breaker behavior for token operations
func TestCircuitBreakerTokenOperations(t *testing.T) {
t.Run("circuit_breaker_prevents_excessive_token_refresh_attempts", func(t *testing.T) {
// Mock base recovery mechanism
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
// Mock logger
logger := &mockLogger{}
// Configure circuit breaker with low failure threshold for testing
config := circuit_breaker.CircuitBreakerConfig{
MaxFailures: 2,
Timeout: 100 * time.Millisecond,
ResetTimeout: 50 * time.Millisecond,
}
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
// Simulate token refresh failures
tokenRefreshError := errors.New("token refresh failed: server unavailable")
failingTokenRefresh := func() error {
// Simulate token refresh operation that fails
return tokenRefreshError
}
// Execute failing token refresh operations to trigger circuit breaker
ctx := context.Background()
var firstError, secondError, thirdError error
// First failure
firstError = cb.ExecuteWithContext(ctx, failingTokenRefresh)
assert.Equal(t, tokenRefreshError, firstError, "First attempt should return the actual error")
assert.Equal(t, circuit_breaker.CircuitBreakerClosed, cb.GetState(), "Circuit should remain closed after first failure")
// Second failure - should open the circuit
secondError = cb.ExecuteWithContext(ctx, failingTokenRefresh)
assert.Equal(t, tokenRefreshError, secondError, "Second attempt should return the actual error")
assert.Equal(t, circuit_breaker.CircuitBreakerOpen, cb.GetState(), "Circuit should be open after max failures")
// Third attempt - should be blocked by circuit breaker
callCount := 0
blockedTokenRefresh := func() error {
callCount++
return nil // This shouldn't be called
}
thirdError = cb.ExecuteWithContext(ctx, blockedTokenRefresh)
assert.Error(t, thirdError, "Third attempt should be blocked")
assert.Contains(t, thirdError.Error(), "circuit breaker is open", "Error should indicate circuit breaker is open")
assert.Equal(t, 0, callCount, "Token refresh function should not be called when circuit is open")
// Verify metrics
metrics := cb.GetMetrics()
assert.Equal(t, "open", metrics["state"], "Circuit breaker state should be open")
assert.Equal(t, int64(2), metrics["current_failures"], "Should record 2 failures")
assert.Equal(t, int64(3), baseRecovery.getRequestCount(), "Should record 3 requests (including blocked one)")
assert.Equal(t, int64(2), atomic.LoadInt64(&baseRecovery.failureCount), "Should record 2 failures")
})
t.Run("circuit_breaker_allows_gradual_recovery_for_token_operations", func(t *testing.T) {
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
logger := &mockLogger{}
config := circuit_breaker.CircuitBreakerConfig{
MaxFailures: 1,
Timeout: 10 * time.Millisecond, // Short timeout for testing
ResetTimeout: 10 * time.Millisecond,
}
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
// Trigger circuit opening
failingOperation := func() error {
return errors.New("token server unavailable")
}
err := cb.ExecuteWithContext(context.Background(), failingOperation)
assert.Error(t, err, "First attempt should fail")
assert.Equal(t, circuit_breaker.CircuitBreakerOpen, cb.GetState(), "Circuit should be open")
// Wait for timeout to allow half-open transition
time.Sleep(15 * time.Millisecond)
// Successful token operation should close circuit
successfulTokenRefresh := func() error {
// Simulate successful token refresh
return nil
}
err = cb.ExecuteWithContext(context.Background(), successfulTokenRefresh)
assert.NoError(t, err, "Successful operation should work in half-open state")
assert.Equal(t, circuit_breaker.CircuitBreakerClosed, cb.GetState(), "Circuit should be closed after success")
// Subsequent operations should work normally
err = cb.ExecuteWithContext(context.Background(), successfulTokenRefresh)
assert.NoError(t, err, "Operations should work normally after recovery")
// Verify recovery was logged
infoLogs := baseRecovery.getInfoLogs()
assert.Greater(t, len(infoLogs), 0, "Should have logged recovery")
})
t.Run("circuit_breaker_handles_concurrent_token_operations", func(t *testing.T) {
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
logger := &mockLogger{}
config := circuit_breaker.CircuitBreakerConfig{
MaxFailures: 5, // Higher threshold for concurrency test
Timeout: 100 * time.Millisecond,
ResetTimeout: 50 * time.Millisecond,
}
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
// Simulate concurrent token operations with mixed success/failure
var wg sync.WaitGroup
successCount := int64(0)
errorCount := int64(0)
circuitOpenCount := int64(0)
operationCount := 20
for i := 0; i < operationCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
tokenOperation := func() error {
// Simulate some failures
if id%4 == 0 {
return errors.New("token operation failed")
}
// Simulate processing time
time.Sleep(time.Millisecond)
return nil
}
err := cb.ExecuteWithContext(context.Background(), tokenOperation)
if err != nil {
if err.Error() == "circuit breaker is open" {
atomic.AddInt64(&circuitOpenCount, 1)
} else {
atomic.AddInt64(&errorCount, 1)
}
} else {
atomic.AddInt64(&successCount, 1)
}
}(i)
}
wg.Wait()
finalSuccess := atomic.LoadInt64(&successCount)
finalError := atomic.LoadInt64(&errorCount)
finalCircuitOpen := atomic.LoadInt64(&circuitOpenCount)
assert.Greater(t, finalSuccess, int64(0), "Should have some successful operations")
totalProcessed := finalSuccess + finalError + finalCircuitOpen
assert.LessOrEqual(t, totalProcessed, int64(operationCount), "Should not exceed total operations")
// Verify final state and metrics
metrics := cb.GetMetrics()
t.Logf("Final state: %s, successes: %d, errors: %d, circuit blocked: %d",
metrics["state"], finalSuccess, finalError, finalCircuitOpen)
})
t.Run("circuit_breaker_respects_timeout_for_token_operations", func(t *testing.T) {
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
logger := &mockLogger{}
config := circuit_breaker.CircuitBreakerConfig{
MaxFailures: 1,
Timeout: 50 * time.Millisecond, // Specific timeout for testing
ResetTimeout: 25 * time.Millisecond,
}
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
// Open the circuit
err := cb.ExecuteWithContext(context.Background(), func() error {
return errors.New("initial failure")
})
assert.Error(t, err)
assert.Equal(t, circuit_breaker.CircuitBreakerOpen, cb.GetState())
// Should be blocked before timeout
err = cb.ExecuteWithContext(context.Background(), func() error {
return nil
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "circuit breaker is open")
// Wait for timeout
time.Sleep(60 * time.Millisecond)
// Should be available after timeout (half-open)
available := cb.IsAvailable()
assert.True(t, available, "Circuit breaker should be available after timeout")
// Successful operation should close the circuit
err = cb.ExecuteWithContext(context.Background(), func() error {
return nil // Success
})
assert.NoError(t, err)
assert.Equal(t, circuit_breaker.CircuitBreakerClosed, cb.GetState())
})
}
// TestMetadataCacheGracePeriodExtensions tests metadata cache grace period behavior during outages
func TestMetadataCacheGracePeriodExtensions(t *testing.T) {
t.Run("cache_extends_grace_period_during_provider_outage", func(t *testing.T) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(10, 1, logger)
defer cache.Clear()
// Initial metadata with short TTL
metadata := &ProviderMetadata{
Issuer: "https://test-provider.com",
AuthURL: "https://test-provider.com/auth",
TokenURL: "https://test-provider.com/token",
JWKSURL: "https://test-provider.com/jwks",
}
initialTTL := 100 * time.Millisecond
cache.Set("test-provider", metadata, initialTTL)
// Verify initial retrieval
retrieved, found := cache.Get("test-provider")
require.True(t, found, "Should find initial metadata")
assert.Equal(t, metadata.Issuer, retrieved.Issuer)
// Wait for initial TTL to expire
time.Sleep(120 * time.Millisecond)
// Should be expired by now
_, found = cache.Get("test-provider")
assert.False(t, found, "Metadata should be expired after TTL")
// Simulate grace period extension by re-adding with longer TTL
gracePeriodTTL := 5 * time.Minute // Extended grace period
cache.Set("test-provider", metadata, gracePeriodTTL)
// Should be available during grace period
retrieved, found = cache.Get("test-provider")
assert.True(t, found, "Should find metadata during grace period")
assert.Equal(t, metadata.Issuer, retrieved.Issuer)
// Verify the cache maintains the entry longer
time.Sleep(50 * time.Millisecond) // Still within grace period
_, found = cache.Get("test-provider")
assert.True(t, found, "Should still find metadata in extended grace period")
})
t.Run("cache_progressive_grace_period_extension", func(t *testing.T) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(10, 1, logger)
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://progressive-test.com",
AuthURL: "https://progressive-test.com/auth",
TokenURL: "https://progressive-test.com/token",
}
// Simulate progressive grace period extensions: 5min -> 15min -> 30min+
gracePeriods := []time.Duration{
5 * time.Minute, // First extension
15 * time.Minute, // Second extension
30 * time.Minute, // Final extension
}
for i, gracePeriod := range gracePeriods {
key := fmt.Sprintf("progressive-provider-%d", i)
cache.Set(key, metadata, gracePeriod)
// Verify each grace period is set correctly
retrieved, found := cache.Get(key)
require.True(t, found, "Should find metadata for grace period %d", i)
assert.Equal(t, metadata.Issuer, retrieved.Issuer)
}
// Verify all entries exist
stats := cache.GetStats()
entries, ok := stats["entries"].(int64)
if !ok {
// Try int for backward compatibility
if entriesInt, ok := stats["entries"].(int); ok {
entries = int64(entriesInt)
} else {
t.Fatalf("entries field is not int64 or int: %T", stats["entries"])
}
}
assert.Equal(t, int64(3), entries, "Should have 3 entries with different grace periods")
// Test that longer grace periods are indeed longer
time.Sleep(100 * time.Millisecond)
for i := 0; i < 3; i++ {
key := fmt.Sprintf("progressive-provider-%d", i)
_, found := cache.Get(key)
assert.True(t, found, "All entries should still be valid after short delay")
}
})
t.Run("cache_handles_outage_with_memory_pressure", func(t *testing.T) {
// Very small cache to test memory pressure during outages
logger := NewLogger("debug")
cache := NewFixedMetadataCache(3, 1, logger) // Very small limits
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://memory-pressure-test.com",
AuthURL: "https://memory-pressure-test.com/auth",
TokenURL: "https://memory-pressure-test.com/token",
}
// Add entries up to limit with grace period
gracePeriod := 30 * time.Minute
for i := 0; i < 3; i++ {
key := fmt.Sprintf("pressure-provider-%d", i)
cache.Set(key, metadata, gracePeriod)
}
// Verify all entries are present
for i := 0; i < 3; i++ {
key := fmt.Sprintf("pressure-provider-%d", i)
_, found := cache.Get(key)
assert.True(t, found, "Entry %d should be present", i)
}
// Add one more entry, should trigger eviction
cache.Set("pressure-provider-overflow", metadata, gracePeriod)
// Verify eviction occurred (should have max 3 entries)
stats := cache.GetStats()
entries := stats["entries"].(int64)
evictions := stats["evictions"].(int64)
assert.LessOrEqual(t, entries, int64(3), "Should not exceed max entries")
assert.Greater(t, evictions, int64(0), "Should have evictions due to overflow")
})
t.Run("cache_cleanup_respects_extended_grace_periods", func(t *testing.T) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(10, 1, logger)
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://cleanup-test.com",
}
// Add entry with very short TTL (should be cleaned up)
cache.Set("short-ttl", metadata, 10*time.Millisecond)
// Add entry with long grace period (should survive cleanup)
cache.Set("long-grace", metadata, 30*time.Minute)
// Wait for short TTL to expire
time.Sleep(20 * time.Millisecond)
// Trigger cleanup
cache.CleanupExpired()
// Verify short TTL entry was cleaned up
_, found := cache.Get("short-ttl")
assert.False(t, found, "Short TTL entry should be cleaned up")
// Verify long grace period entry survives
_, found = cache.Get("long-grace")
assert.True(t, found, "Long grace period entry should survive cleanup")
// Verify stats reflect cleanup
stats := cache.GetStats()
entries := stats["entries"].(int64)
assert.Equal(t, int64(1), entries, "Should have 1 entry remaining after cleanup")
})
t.Run("cache_concurrent_access_during_grace_period", func(t *testing.T) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(20, 2, logger)
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://concurrent-test.com",
AuthURL: "https://concurrent-test.com/auth",
TokenURL: "https://concurrent-test.com/token",
}
gracePeriod := 10 * time.Minute
// Concurrent access during grace period setup
var wg sync.WaitGroup
successCount := int64(0)
errorCount := int64(0)
// Set initial entries concurrently
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("concurrent-provider-%d", id)
cache.Set(key, metadata, gracePeriod)
}(i)
}
// Concurrent reads during setup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("concurrent-provider-%d", id)
// Wait a bit for the setter to run
time.Sleep(time.Millisecond)
_, found := cache.Get(key)
if found {
atomic.AddInt64(&successCount, 1)
} else {
atomic.AddInt64(&errorCount, 1)
}
}(i)
}
wg.Wait()
finalSuccess := atomic.LoadInt64(&successCount)
finalError := atomic.LoadInt64(&errorCount)
// Should have mostly successful reads
assert.Greater(t, finalSuccess, int64(5), "Should have several successful concurrent reads")
t.Logf("Concurrent access results: %d successes, %d errors", finalSuccess, finalError)
// Verify cache is still functional
cache.Set("final-test", metadata, gracePeriod)
_, found := cache.Get("final-test")
assert.True(t, found, "Cache should still be functional after concurrent access")
})
}
// TestMemoryUsageImprovementBenchmarks provides benchmarks for memory usage improvements
func TestMemoryUsageImprovementBenchmarks(t *testing.T) {
if testing.Short() {
t.Skip("Skipping memory benchmark tests in short mode")
}
t.Run("benchmark_connection_pool_memory_efficiency", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Measure memory before
runtime.GC()
var memBefore, memAfter runtime.MemStats
runtime.ReadMemStats(&memBefore)
// Create many HTTP clients with pooled transports
clients := make([]*http.Client, 100)
config := DefaultHTTPClientConfig()
for i := 0; i < 100; i++ {
clients[i] = CreatePooledHTTPClient(config)
}
// Use the clients
for i := 0; i < 100; i++ {
resp, err := clients[i].Get(server.URL)
if err == nil {
resp.Body.Close()
}
}
// Measure memory after
runtime.GC()
runtime.ReadMemStats(&memAfter)
// Handle potential overflow in memory calculation
var memGrowthMB float64
if memAfter.HeapAlloc > memBefore.HeapAlloc {
memGrowthMB = float64(memAfter.HeapAlloc-memBefore.HeapAlloc) / 1024 / 1024
} else {
memGrowthMB = 0 // Memory decreased or stayed same
}
t.Logf("Memory growth with pooled connections: %.2f MB", memGrowthMB)
// Should not grow excessively (threshold: 50MB) when measurement is reliable
if memGrowthMB > 0 && memGrowthMB < 1000 { // Reasonable range
assert.Less(t, memGrowthMB, 50.0, "Memory growth should be reasonable with pooled connections")
}
// Cleanup
pool := GetGlobalTransportPool()
pool.Cleanup()
})
t.Run("benchmark_bounded_cache_memory_efficiency", func(t *testing.T) {
runtime.GC()
var memBefore, memAfter runtime.MemStats
runtime.ReadMemStats(&memBefore)
// Create bounded cache and fill it beyond limits
logger := NewLogger("debug")
cache := NewFixedMetadataCache(100, 1, logger) // 1MB limit
defer cache.Clear()
// Create large metadata entries
metadata := &ProviderMetadata{
Issuer: string(make([]byte, 1024)), // 1KB
AuthURL: string(make([]byte, 1024)),
TokenURL: string(make([]byte, 1024)),
}
// Add many entries (should trigger eviction)
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("large-entry-%d", i)
cache.Set(key, metadata, time.Hour)
}
runtime.GC()
runtime.ReadMemStats(&memAfter)
// Handle potential overflow in memory calculation
var memGrowthMB float64
if memAfter.HeapAlloc > memBefore.HeapAlloc {
memGrowthMB = float64(memAfter.HeapAlloc-memBefore.HeapAlloc) / 1024 / 1024
} else {
memGrowthMB = 0 // Memory decreased or stayed same
}
t.Logf("Memory growth with bounded cache: %.2f MB", memGrowthMB)
// Verify cache stats
stats := cache.GetStats()
entries := stats["entries"].(int64)
evictions := stats["evictions"].(int64)
memoryUsage := stats["memory"].(int64)
maxMemory := stats["max_memory"].(int64)
t.Logf("Cache stats - entries: %d, evictions: %d, memory: %d/%d bytes",
entries, evictions, memoryUsage, maxMemory)
// Should respect memory limits
assert.LessOrEqual(t, memoryUsage, maxMemory, "Should not exceed memory limit")
assert.Greater(t, evictions, int64(0), "Should have evictions")
// Memory calculation can be unreliable in tests, just verify bounds are working
if memGrowthMB > 0 && memGrowthMB < 1000 { // Reasonable range
assert.Less(t, memGrowthMB, 50.0, "Memory growth should be bounded when measurable")
}
})
t.Run("benchmark_circuit_breaker_memory_efficiency", func(t *testing.T) {
runtime.GC()
var memBefore, memAfter runtime.MemStats
runtime.ReadMemStats(&memBefore)
// Create many circuit breakers
breakers := make([]*circuit_breaker.CircuitBreaker, 100)
baseRecoveries := make([]*mockBaseRecoveryMechanism, 100)
loggers := make([]*mockLogger, 100)
config := circuit_breaker.DefaultCircuitBreakerConfig()
for i := 0; i < 100; i++ {
baseRecoveries[i] = &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
loggers[i] = &mockLogger{}
breakers[i] = circuit_breaker.NewCircuitBreaker(config, loggers[i], baseRecoveries[i])
}
// Exercise the circuit breakers
for i := 0; i < 100; i++ {
// Some operations that succeed
for j := 0; j < 10; j++ {
breakers[i].Execute(func() error { return nil })
}
// Some operations that fail
for j := 0; j < 3; j++ {
breakers[i].Execute(func() error { return errors.New("test error") })
}
// Get metrics (this allocates)
_ = breakers[i].GetMetrics()
}
runtime.GC()
runtime.ReadMemStats(&memAfter)
// Handle potential overflow in memory calculation
var memGrowthMB float64
if memAfter.HeapAlloc > memBefore.HeapAlloc {
memGrowthMB = float64(memAfter.HeapAlloc-memBefore.HeapAlloc) / 1024 / 1024
} else {
memGrowthMB = 0 // Memory decreased or stayed same
}
t.Logf("Memory growth with circuit breakers: %.2f MB", memGrowthMB)
// Circuit breakers should not consume excessive memory when measurement is reliable
if memGrowthMB > 0 && memGrowthMB < 1000 { // Reasonable range
assert.Less(t, memGrowthMB, 10.0, "Circuit breakers should be memory efficient")
}
// Reset and verify all circuit breakers are functional
for i := 0; i < 10; i++ { // Test a sample
breakers[i].Reset() // Reset to closed state
err := breakers[i].Execute(func() error { return nil })
assert.NoError(t, err, "Circuit breaker %d should be functional after reset", i)
}
})
t.Run("benchmark_goroutine_leak_prevention", func(t *testing.T) {
initialGoroutines := runtime.NumGoroutine()
// Create and start multiple background tasks
var wg sync.WaitGroup
tasks := make([]*LazyBackgroundTask, 20)
logger := GetSingletonNoOpLogger()
for i := 0; i < 20; i++ {
taskFunc := func() {
// Minimal task work
time.Sleep(time.Millisecond)
}
tasks[i] = NewLazyBackgroundTask(
fmt.Sprintf("benchmark-task-%d", i),
10*time.Millisecond,
taskFunc,
logger,
&wg,
)
tasks[i].StartIfNeeded()
}
// Let tasks run briefly
time.Sleep(50 * time.Millisecond)
// Stop all tasks
for _, task := range tasks {
task.Stop()
}
// Wait for cleanup
wg.Wait()
time.Sleep(100 * time.Millisecond)
runtime.GC()
finalGoroutines := runtime.NumGoroutine()
goroutineGrowth := finalGoroutines - initialGoroutines
t.Logf("Goroutine count - initial: %d, final: %d, growth: %d",
initialGoroutines, finalGoroutines, goroutineGrowth)
// Should not have significant goroutine leaks
assert.LessOrEqual(t, goroutineGrowth, 5, "Goroutine growth should be minimal after cleanup")
})
}
// TestRaceConditionDetection tests for race conditions in memory leak fixes
func TestRaceConditionDetection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping race condition tests in short mode")
}
t.Run("connection_pool_race_conditions", func(t *testing.T) {
pool := GetGlobalTransportPool()
defer pool.Cleanup()
var wg sync.WaitGroup
operationCount := 50
// Concurrent transport creation and release
for i := 0; i < operationCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
config := DefaultHTTPClientConfig()
config.MaxConnsPerHost = id % 5 // Create some variety
// Get transport
transport := pool.GetOrCreateTransport(config)
assert.NotNil(t, transport, "Should get a valid transport")
// Use transport briefly
time.Sleep(time.Millisecond)
// Release transport
pool.ReleaseTransport(transport)
}(i)
}
// Concurrent cleanup operations
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(25 * time.Millisecond)
// This tests concurrent cleanup
pool.mu.RLock()
transportCount := len(pool.transports)
pool.mu.RUnlock()
assert.GreaterOrEqual(t, transportCount, 0, "Transport count should be non-negative")
}()
}
wg.Wait()
})
t.Run("circuit_breaker_race_conditions", func(t *testing.T) {
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
logger := &mockLogger{}
config := circuit_breaker.DefaultCircuitBreakerConfig()
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
var wg sync.WaitGroup
operationCount := 100
successCount := int64(0)
errorCount := int64(0)
// Concurrent executions
for i := 0; i < operationCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
operation := func() error {
if id%10 == 0 {
return errors.New("simulated failure")
}
return nil
}
err := cb.ExecuteWithContext(context.Background(), operation)
if err != nil {
atomic.AddInt64(&errorCount, 1)
} else {
atomic.AddInt64(&successCount, 1)
}
}(i)
}
// Concurrent state checks
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = cb.GetState()
_ = cb.IsAvailable()
_ = cb.GetMetrics()
_ = cb.GetFailureCount()
}()
}
// Concurrent resets
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(50 * time.Millisecond)
cb.Reset()
}()
}
wg.Wait()
finalSuccess := atomic.LoadInt64(&successCount)
finalError := atomic.LoadInt64(&errorCount)
assert.Greater(t, finalSuccess, int64(0), "Should have successful operations")
t.Logf("Race condition test results: %d successes, %d errors", finalSuccess, finalError)
})
t.Run("metadata_cache_race_conditions", func(t *testing.T) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(50, 2, logger)
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://race-test.com",
AuthURL: "https://race-test.com/auth",
TokenURL: "https://race-test.com/token",
}
var wg sync.WaitGroup
operationCount := 100
// Concurrent sets
for i := 0; i < operationCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("race-key-%d", id%20) // Some key overlap
cache.Set(key, metadata, time.Minute)
}(i)
}
// Concurrent gets
for i := 0; i < operationCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("race-key-%d", id%20)
_, _ = cache.Get(key) // Ignore result, just test for races
}(i)
}
// Concurrent deletes
for i := 0; i < operationCount/4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("race-key-%d", id%20)
cache.Delete(key)
}(i)
}
// Concurrent stats and cleanup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = cache.GetStats()
cache.CleanupExpired()
}()
}
wg.Wait()
// Verify cache is still functional
cache.Set("final-race-test", metadata, time.Minute)
_, found := cache.Get("final-race-test")
assert.True(t, found, "Cache should be functional after race condition test")
})
t.Run("lazy_background_task_race_conditions", func(t *testing.T) {
var mainWG sync.WaitGroup
taskCount := 10 // Reduced count to avoid timeout
tasks := make([]*LazyBackgroundTask, taskCount)
logger := GetSingletonNoOpLogger()
callCounts := make([]int64, taskCount)
for i := 0; i < taskCount; i++ {
taskID := i
taskFunc := func() {
atomic.AddInt64(&callCounts[taskID], 1)
}
// Use separate WaitGroups for each task to avoid conflicts
var taskWG sync.WaitGroup
tasks[i] = NewLazyBackgroundTask(
fmt.Sprintf("race-task-%d", i),
50*time.Millisecond, // Longer interval to reduce contention
taskFunc,
logger,
&taskWG,
)
}
// Concurrent start operations
for i := 0; i < taskCount; i++ {
mainWG.Add(1)
go func(id int) {
defer mainWG.Done()
tasks[id].StartIfNeeded()
// Multiple start attempts to test sync.Once
tasks[id].StartIfNeeded()
tasks[id].StartIfNeeded()
}(i)
}
mainWG.Wait()
// Let tasks run briefly
time.Sleep(100 * time.Millisecond)
// Concurrent stop operations
for i := 0; i < taskCount; i++ {
mainWG.Add(1)
go func(id int) {
defer mainWG.Done()
tasks[id].Stop()
}(i)
}
mainWG.Wait()
// Verify most tasks executed (some may not have had time to run)
executedCount := 0
for i := 0; i < taskCount; i++ {
callCount := atomic.LoadInt64(&callCounts[i])
if callCount > 0 {
executedCount++
}
}
assert.Greater(t, executedCount, taskCount/2, "At least half the tasks should have executed")
})
}
// Mock implementations for testing
type mockBaseRecoveryMechanism struct {
requestCount int64
successCount int64
failureCount int64
infoLogs []string
errorLogs []string
debugLogs []string
baseMetrics map[string]interface{}
mu sync.RWMutex
}
func (m *mockBaseRecoveryMechanism) RecordRequest() {
atomic.AddInt64(&m.requestCount, 1)
}
func (m *mockBaseRecoveryMechanism) RecordSuccess() {
atomic.AddInt64(&m.successCount, 1)
}
func (m *mockBaseRecoveryMechanism) RecordFailure() {
atomic.AddInt64(&m.failureCount, 1)
}
func (m *mockBaseRecoveryMechanism) GetBaseMetrics() map[string]interface{} {
m.mu.RLock()
defer m.mu.RUnlock()
result := make(map[string]interface{})
for k, v := range m.baseMetrics {
result[k] = v
}
result["total_requests"] = atomic.LoadInt64(&m.requestCount)
result["total_successes"] = atomic.LoadInt64(&m.successCount)
result["total_failures"] = atomic.LoadInt64(&m.failureCount)
return result
}
func (m *mockBaseRecoveryMechanism) LogInfo(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.infoLogs = append(m.infoLogs, fmt.Sprintf(format, args...))
}
func (m *mockBaseRecoveryMechanism) LogError(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.errorLogs = append(m.errorLogs, fmt.Sprintf(format, args...))
}
func (m *mockBaseRecoveryMechanism) LogDebug(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.debugLogs = append(m.debugLogs, fmt.Sprintf(format, args...))
}
func (m *mockBaseRecoveryMechanism) getRequestCount() int64 {
return atomic.LoadInt64(&m.requestCount)
}
func (m *mockBaseRecoveryMechanism) getInfoLogs() []string {
m.mu.RLock()
defer m.mu.RUnlock()
result := make([]string, len(m.infoLogs))
copy(result, m.infoLogs)
return result
}
type mockLogger struct {
infoLogs []string
errorLogs []string
debugLogs []string
mu sync.RWMutex
}
func (m *mockLogger) Infof(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.infoLogs = append(m.infoLogs, fmt.Sprintf(format, args...))
}
func (m *mockLogger) Errorf(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.errorLogs = append(m.errorLogs, fmt.Sprintf(format, args...))
}
func (m *mockLogger) Debugf(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.debugLogs = append(m.debugLogs, fmt.Sprintf(format, args...))
}
// BenchmarkConnectivityIssuesFixes provides performance benchmarks for connectivity fixes
func BenchmarkConnectivityIssuesFixes(b *testing.B) {
b.Run("ConnectionPooling", func(b *testing.B) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
pool := GetGlobalTransportPool()
defer pool.Cleanup()
config := DefaultHTTPClientConfig()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
client := CreatePooledHTTPClient(config)
resp, err := client.Get(server.URL)
if err == nil {
resp.Body.Close()
}
}
})
})
b.Run("CircuitBreakerOperations", func(b *testing.B) {
baseRecovery := &mockBaseRecoveryMechanism{
baseMetrics: make(map[string]interface{}),
}
logger := &mockLogger{}
config := circuit_breaker.DefaultCircuitBreakerConfig()
cb := circuit_breaker.NewCircuitBreaker(config, logger, baseRecovery)
operation := func() error {
return nil // Always succeed for benchmark
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
cb.ExecuteWithContext(context.Background(), operation)
}
})
})
b.Run("MetadataCacheOperations", func(b *testing.B) {
logger := NewLogger("debug")
cache := NewFixedMetadataCache(1000, 10, logger)
defer cache.Clear()
metadata := &ProviderMetadata{
Issuer: "https://benchmark-test.com",
AuthURL: "https://benchmark-test.com/auth",
TokenURL: "https://benchmark-test.com/token",
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Sprintf("bench-key-%d", i%100)
if i%2 == 0 {
cache.Set(key, metadata, time.Hour)
} else {
cache.Get(key)
}
i++
}
})
})
b.Run("LazyBackgroundTaskOperations", func(b *testing.B) {
logger := GetSingletonNoOpLogger()
taskFunc := func() {
// Minimal work
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
task := NewLazyBackgroundTask("bench-task", time.Second, taskFunc, logger, &wg)
task.StartIfNeeded()
task.Stop()
}
})
}