Merge main into 0.8.0-redis: resolve conflicts

Merged the following features from main:
- Dynamic Client Registration (RFC 7591)
- Sharded cache and CPU spike prevention
- Documentation updates (CNAME, index.html)

Resolved conflicts in:
- settings.go: Combined Redis config with Dynamic Client Registration config
- universal_cache_singleton.go: Merged Redis backend support with consolidated cleanup

Both features now work together:
- Redis distributed caching with circuit breaker and health checks
- Consolidated cleanup goroutine (reduces from 7 to 1)
- SkipAutoCleanup flag honored for all cache types
This commit is contained in:
2025-11-30 02:03:13 +00:00
23 changed files with 4296 additions and 214 deletions
+161 -56
View File
@@ -1,6 +1,7 @@
package traefikoidc
import (
"context"
"sync"
"time"
@@ -9,6 +10,8 @@ import (
)
// UniversalCacheManager manages all cache instances using the universal cache
// It runs a single consolidated cleanup goroutine for all caches, reducing
// goroutine count and CPU overhead compared to per-cache cleanup routines.
type UniversalCacheManager struct {
tokenCache *UniversalCache
blacklistCache *UniversalCache
@@ -20,6 +23,12 @@ type UniversalCacheManager struct {
sharedBackend backends.CacheBackend // Shared backend (Redis) that should be closed by manager, not individual caches
mu sync.RWMutex
logger *Logger
// Consolidated cleanup management
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
cleanupStarted bool
}
var (
@@ -34,12 +43,20 @@ func GetUniversalCacheManager(logger *Logger) *UniversalCacheManager {
logger = GetSingletonNoOpLogger()
}
ctx, cancel := context.WithCancel(context.Background())
universalCacheManager = &UniversalCacheManager{
logger: logger,
ctx: ctx,
cancel: cancel,
}
// Initialize with default in-memory backends
initializeDefaultCaches(universalCacheManager, logger)
// Start single consolidated cleanup goroutine for all caches
// This replaces 7 individual cleanup goroutines with 1
universalCacheManager.startConsolidatedCleanup()
})
return universalCacheManager
@@ -52,8 +69,12 @@ func GetUniversalCacheManagerWithConfig(logger *Logger, redisConfig *RedisConfig
logger = GetSingletonNoOpLogger()
}
ctx, cancel := context.WithCancel(context.Background())
universalCacheManager = &UniversalCacheManager{
logger: logger,
ctx: ctx,
cancel: cancel,
}
if redisConfig != nil && redisConfig.Enabled {
@@ -63,6 +84,10 @@ func GetUniversalCacheManagerWithConfig(logger *Logger, redisConfig *RedisConfig
logger.Info("Initializing cache manager with memory-only backend")
initializeDefaultCaches(universalCacheManager, logger)
}
// Start single consolidated cleanup goroutine for all caches
// This replaces 7 individual cleanup goroutines with 1
universalCacheManager.startConsolidatedCleanup()
})
return universalCacheManager
@@ -72,19 +97,21 @@ func GetUniversalCacheManagerWithConfig(logger *Logger, redisConfig *RedisConfig
func initializeDefaultCaches(manager *UniversalCacheManager, logger *Logger) {
// Initialize token cache - CRITICAL FIX: Reduced from 5000 to 1000
manager.tokenCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 1000, // CRITICAL FIX: Reduced from 5000 to 1000 items
MaxMemoryBytes: 5 * 1024 * 1024, // CRITICAL FIX: Added 5MB memory limit
DefaultTTL: 1 * time.Hour,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 1000, // CRITICAL FIX: Reduced from 5000 to 1000 items
MaxMemoryBytes: 5 * 1024 * 1024, // CRITICAL FIX: Added 5MB memory limit
DefaultTTL: 1 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize blacklist cache
manager.blacklistCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 24 * time.Hour,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 24 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize metadata cache with grace periods
@@ -104,40 +131,45 @@ func initializeDefaultCaches(manager *UniversalCacheManager, logger *Logger) {
"issuer",
},
},
Logger: logger,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize JWK cache
manager.jwkCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeJWK,
MaxSize: 200,
DefaultTTL: 1 * time.Hour,
Logger: logger,
Type: CacheTypeJWK,
MaxSize: 200,
DefaultTTL: 1 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize session cache - CRITICAL FIX: Reduced from 10000 to 2000
manager.sessionCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeSession,
MaxSize: 2000, // CRITICAL FIX: Reduced from 10000 to 2000 items
MaxMemoryBytes: 5 * 1024 * 1024, // CRITICAL FIX: Added 5MB memory limit
DefaultTTL: 30 * time.Minute,
Logger: logger,
Type: CacheTypeSession,
MaxSize: 2000, // CRITICAL FIX: Reduced from 10000 to 2000 items
MaxMemoryBytes: 5 * 1024 * 1024, // CRITICAL FIX: Added 5MB memory limit
DefaultTTL: 30 * time.Minute,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize introspection cache for OAuth 2.0 Token Introspection (RFC 7662)
manager.introspectionCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeToken, // Use token cache type for introspection results
MaxSize: 1000, // Cache up to 1000 introspection results
DefaultTTL: 5 * time.Minute, // Short TTL for security (introspect frequently)
Logger: logger,
Type: CacheTypeToken, // Use token cache type for introspection results
MaxSize: 1000, // Cache up to 1000 introspection results
DefaultTTL: 5 * time.Minute, // Short TTL for security (introspect frequently)
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Initialize token type cache for performance optimization
manager.tokenTypeCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeToken, // Use token cache type for token type detection
MaxSize: 2000, // Cache up to 2000 token type detections
DefaultTTL: 5 * time.Minute, // 5 minute TTL for token type detection
Logger: logger,
Type: CacheTypeToken, // Use token cache type for token type detection
MaxSize: 2000, // Cache up to 2000 token type detections
DefaultTTL: 5 * time.Minute, // 5 minute TTL for token type detection
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
}
@@ -230,11 +262,12 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r
// Initialize token cache with backend
manager.tokenCache = NewUniversalCacheWithBackend(
UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 1000,
MaxMemoryBytes: 5 * 1024 * 1024,
DefaultTTL: 1 * time.Hour,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 1000,
MaxMemoryBytes: 5 * 1024 * 1024,
DefaultTTL: 1 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
},
createBackend(CacheTypeToken),
)
@@ -242,10 +275,11 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r
// Initialize blacklist cache (CRITICAL - must be consistent across replicas)
manager.blacklistCache = NewUniversalCacheWithBackend(
UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 24 * time.Hour,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 24 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
},
createBackend("blacklist"),
)
@@ -268,7 +302,8 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r
"issuer",
},
},
Logger: logger,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
},
createBackend(CacheTypeMetadata),
)
@@ -276,45 +311,106 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r
// Initialize JWK cache
manager.jwkCache = NewUniversalCacheWithBackend(
UniversalCacheConfig{
Type: CacheTypeJWK,
MaxSize: 200,
DefaultTTL: 1 * time.Hour,
Logger: logger,
Type: CacheTypeJWK,
MaxSize: 200,
DefaultTTL: 1 * time.Hour,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
},
createBackend(CacheTypeJWK),
)
// Session cache stays memory-only (high volume, local state)
manager.sessionCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeSession,
MaxSize: 2000,
MaxMemoryBytes: 5 * 1024 * 1024,
DefaultTTL: 30 * time.Minute,
Logger: logger,
Type: CacheTypeSession,
MaxSize: 2000,
MaxMemoryBytes: 5 * 1024 * 1024,
DefaultTTL: 30 * time.Minute,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
// Introspection cache uses backend for sharing results
manager.introspectionCache = NewUniversalCacheWithBackend(
UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 5 * time.Minute,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 1000,
DefaultTTL: 5 * time.Minute,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
},
createBackend(CacheTypeToken),
)
// Token type cache stays memory-only (local optimization)
manager.tokenTypeCache = NewUniversalCache(UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 2000,
DefaultTTL: 5 * time.Minute,
Logger: logger,
Type: CacheTypeToken,
MaxSize: 2000,
DefaultTTL: 5 * time.Minute,
Logger: logger,
SkipAutoCleanup: true, // Managed cleanup
})
logger.Infof("Cache manager initialized with %s backend configuration", redisConfig.CacheMode)
}
// startConsolidatedCleanup starts a single cleanup goroutine for all caches
// This reduces goroutine count from 7 to 1 and consolidates cleanup operations
func (m *UniversalCacheManager) startConsolidatedCleanup() {
m.mu.Lock()
if m.cleanupStarted {
m.mu.Unlock()
return
}
m.cleanupStarted = true
m.mu.Unlock()
m.wg.Add(1)
go func() {
defer m.wg.Done()
// Use 5-minute interval for consolidated cleanup
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
m.performConsolidatedCleanup()
}
}
}()
m.logger.Info("UniversalCacheManager: Started consolidated cleanup routine for all caches")
}
// performConsolidatedCleanup runs cleanup on all caches in sequence
// This is more efficient than parallel cleanup as it reduces lock contention
func (m *UniversalCacheManager) performConsolidatedCleanup() {
m.mu.RLock()
caches := []*UniversalCache{
m.tokenCache,
m.blacklistCache,
m.metadataCache,
m.jwkCache,
m.sessionCache,
m.introspectionCache,
m.tokenTypeCache,
}
m.mu.RUnlock()
for _, cache := range caches {
if cache != nil {
// Each cache.Cleanup() is self-contained and handles its own locking
cache.Cleanup()
}
}
m.logger.Debugf("UniversalCacheManager: Consolidated cleanup completed for all caches")
}
// GetTokenCache returns the token cache
func (m *UniversalCacheManager) GetTokenCache() *UniversalCache {
m.mu.RLock()
@@ -364,8 +460,16 @@ func (m *UniversalCacheManager) GetTokenTypeCache() *UniversalCache {
return m.tokenTypeCache
}
// Close shuts down all caches
// Close shuts down all caches and the consolidated cleanup routine
func (m *UniversalCacheManager) Close() error {
// Stop the consolidated cleanup routine first
if m.cancel != nil {
m.cancel()
}
// Wait for cleanup routine to finish
m.wg.Wait()
m.mu.Lock()
defer m.mu.Unlock()
@@ -387,7 +491,8 @@ func (m *UniversalCacheManager) Close() error {
}
}
m.logger.Info("UniversalCacheManager: Closed all caches")
m.cleanupStarted = false
m.logger.Info("UniversalCacheManager: Closed all caches and cleanup routine")
return nil
}