diff --git a/universal_cache.go b/universal_cache.go index 014494a..4e65ccf 100644 --- a/universal_cache.go +++ b/universal_cache.go @@ -93,7 +93,8 @@ type UniversalCache struct { logger *Logger // Backend for distributed caching (NEW) - backend backends.CacheBackend + backend backends.CacheBackend + ownsBackend bool // If true, cache should close backend on Close(); if false, backend is shared // Memory management currentSize int64 @@ -120,6 +121,7 @@ func NewUniversalCache(config UniversalCacheConfig) *UniversalCache { func NewUniversalCacheWithBackend(config UniversalCacheConfig, cacheBackend backends.CacheBackend) *UniversalCache { cache := createUniversalCache(config) cache.backend = cacheBackend + cache.ownsBackend = false // Shared backend, managed externally return cache } @@ -517,8 +519,8 @@ func (c *UniversalCache) Close() error { // Clear all items c.Clear() - // Close backend if present - if c.backend != nil { + // Close backend only if this cache owns it (not shared) + if c.backend != nil && c.ownsBackend { if err := c.backend.Close(); err != nil { c.logger.Infof("Failed to close cache backend: %v", err) } diff --git a/universal_cache_singleton.go b/universal_cache_singleton.go index 1130033..6e53d81 100644 --- a/universal_cache_singleton.go +++ b/universal_cache_singleton.go @@ -15,8 +15,9 @@ type UniversalCacheManager struct { metadataCache *UniversalCache jwkCache *UniversalCache sessionCache *UniversalCache - introspectionCache *UniversalCache // OAuth 2.0 Token Introspection cache (RFC 7662) - tokenTypeCache *UniversalCache // Cache for token type detection results + introspectionCache *UniversalCache // OAuth 2.0 Token Introspection cache (RFC 7662) + tokenTypeCache *UniversalCache // Cache for token type detection results + sharedBackend backends.CacheBackend // Shared backend (Redis) that should be closed by manager, not individual caches mu sync.RWMutex logger *Logger } @@ -156,17 +157,18 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r EnableMetrics: true, } - var redisBackend backends.CacheBackend - var err error - - // Create Redis backend with resilience features if enabled - redisBackend, err = backends.NewRedisBackend(redisBackendConfig) + // Use concrete type to avoid Yaegi reflection issues with interface assignment + // The concrete type will be automatically converted to interface when needed + baseBackend, err := backends.NewRedisBackend(redisBackendConfig) if err != nil { logger.Errorf("Failed to create Redis backend: %v. Falling back to memory-only mode.", err) initializeDefaultCaches(manager, logger) return } + // Build the backend with optional wrappers + var redisBackend backends.CacheBackend = baseBackend + // Wrap with circuit breaker if enabled if redisConfig.EnableCircuitBreaker { cbConfig := resilience.DefaultCircuitBreakerConfig() @@ -196,6 +198,9 @@ func initializeCachesWithRedis(manager *UniversalCacheManager, logger *Logger, r logger.Info("Redis backend wrapped with health checker") } + // Store the fully-wrapped shared backend in the manager so it can be closed properly + manager.sharedBackend = redisBackend + // Decide which backend to use based on cache mode var createBackend func(cacheType CacheType) backends.CacheBackend @@ -364,6 +369,7 @@ func (m *UniversalCacheManager) Close() error { m.mu.Lock() defer m.mu.Unlock() + // Close all caches first (they won't close the shared backend) for _, cache := range []*UniversalCache{ m.tokenCache, m.blacklistCache, m.metadataCache, m.jwkCache, m.sessionCache, m.introspectionCache, m.tokenTypeCache, } { @@ -372,6 +378,15 @@ func (m *UniversalCacheManager) Close() error { } } + // Now close the shared backend if present + if m.sharedBackend != nil { + if err := m.sharedBackend.Close(); err != nil { + m.logger.Infof("Failed to close shared cache backend: %v", err) + } else { + m.logger.Info("UniversalCacheManager: Closed shared backend") + } + } + m.logger.Info("UniversalCacheManager: Closed all caches") return nil }