mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
1317 lines
38 KiB
Go
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()
|
|
}
|
|
})
|
|
}
|