package cache import ( "context" "encoding/json" "time" "go.uber.org/zap" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/errors" ) // CacheProvider defines the interface for cache operations type CacheProvider interface { // Get retrieves a value from cache Get(ctx context.Context, key string) ([]byte, error) // Set stores a value in cache with TTL Set(ctx context.Context, key string, value []byte, ttl time.Duration) error // Delete removes a value from cache Delete(ctx context.Context, key string) error // Exists checks if a key exists in cache Exists(ctx context.Context, key string) (bool, error) // Clear removes all cached values (use with caution) Clear(ctx context.Context) error // Close closes the cache connection Close() error } // MemoryCache implements CacheProvider using in-memory storage type MemoryCache struct { data map[string]cacheItem config config.ConfigProvider logger *zap.Logger } type cacheItem struct { Value []byte ExpiresAt time.Time } // NewMemoryCache creates a new in-memory cache func NewMemoryCache(config config.ConfigProvider, logger *zap.Logger) CacheProvider { cache := &MemoryCache{ data: make(map[string]cacheItem), config: config, logger: logger, } // Start cleanup goroutine go cache.cleanup() return cache } // Get retrieves a value from memory cache func (m *MemoryCache) Get(ctx context.Context, key string) ([]byte, error) { m.logger.Debug("Getting value from memory cache", zap.String("key", key)) item, exists := m.data[key] if !exists { return nil, errors.NewNotFoundError("cache key") } // Check if expired if time.Now().After(item.ExpiresAt) { delete(m.data, key) return nil, errors.NewNotFoundError("cache key") } return item.Value, nil } // Set stores a value in memory cache func (m *MemoryCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { m.logger.Debug("Setting value in memory cache", zap.String("key", key), zap.Duration("ttl", ttl)) m.data[key] = cacheItem{ Value: value, ExpiresAt: time.Now().Add(ttl), } return nil } // Delete removes a value from memory cache func (m *MemoryCache) Delete(ctx context.Context, key string) error { m.logger.Debug("Deleting value from memory cache", zap.String("key", key)) delete(m.data, key) return nil } // Exists checks if a key exists in memory cache func (m *MemoryCache) Exists(ctx context.Context, key string) (bool, error) { item, exists := m.data[key] if !exists { return false, nil } // Check if expired if time.Now().After(item.ExpiresAt) { delete(m.data, key) return false, nil } return true, nil } // Clear removes all values from memory cache func (m *MemoryCache) Clear(ctx context.Context) error { m.logger.Debug("Clearing memory cache") m.data = make(map[string]cacheItem) return nil } // Close closes the memory cache (no-op for memory cache) func (m *MemoryCache) Close() error { return nil } // cleanup removes expired items from memory cache func (m *MemoryCache) cleanup() { ticker := time.NewTicker(5 * time.Minute) // Cleanup every 5 minutes defer ticker.Stop() for range ticker.C { now := time.Now() for key, item := range m.data { if now.After(item.ExpiresAt) { delete(m.data, key) } } } } // CacheManager provides high-level caching operations with JSON serialization type CacheManager struct { provider CacheProvider config config.ConfigProvider logger *zap.Logger } // NewCacheManager creates a new cache manager func NewCacheManager(config config.ConfigProvider, logger *zap.Logger) *CacheManager { var provider CacheProvider // Use Redis if configured, otherwise fall back to memory cache if config.GetBool("REDIS_ENABLED") { redisProvider, err := NewRedisCache(config, logger) if err != nil { logger.Warn("Failed to initialize Redis cache, falling back to memory cache", zap.Error(err)) provider = NewMemoryCache(config, logger) } else { provider = redisProvider } } else { provider = NewMemoryCache(config, logger) } return &CacheManager{ provider: provider, config: config, logger: logger, } } // GetJSON retrieves and unmarshals a JSON value from cache func (c *CacheManager) GetJSON(ctx context.Context, key string, dest interface{}) error { c.logger.Debug("Getting JSON from cache", zap.String("key", key)) data, err := c.provider.Get(ctx, key) if err != nil { return err } if err := json.Unmarshal(data, dest); err != nil { c.logger.Error("Failed to unmarshal cached JSON", zap.Error(err)) return errors.NewInternalError("Failed to unmarshal cached data").WithInternal(err) } return nil } // SetJSON marshals and stores a JSON value in cache func (c *CacheManager) SetJSON(ctx context.Context, key string, value interface{}, ttl time.Duration) error { c.logger.Debug("Setting JSON in cache", zap.String("key", key), zap.Duration("ttl", ttl)) data, err := json.Marshal(value) if err != nil { c.logger.Error("Failed to marshal JSON for cache", zap.Error(err)) return errors.NewInternalError("Failed to marshal data for cache").WithInternal(err) } return c.provider.Set(ctx, key, data, ttl) } // Get retrieves raw bytes from cache func (c *CacheManager) Get(ctx context.Context, key string) ([]byte, error) { return c.provider.Get(ctx, key) } // Set stores raw bytes in cache func (c *CacheManager) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { return c.provider.Set(ctx, key, value, ttl) } // Delete removes a value from cache func (c *CacheManager) Delete(ctx context.Context, key string) error { return c.provider.Delete(ctx, key) } // Exists checks if a key exists in cache func (c *CacheManager) Exists(ctx context.Context, key string) (bool, error) { return c.provider.Exists(ctx, key) } // Clear removes all cached values func (c *CacheManager) Clear(ctx context.Context) error { return c.provider.Clear(ctx) } // Close closes the cache connection func (c *CacheManager) Close() error { return c.provider.Close() } // GetDefaultTTL returns the default TTL from config func (c *CacheManager) GetDefaultTTL() time.Duration { return c.config.GetDuration("CACHE_TTL") } // IsEnabled returns whether caching is enabled func (c *CacheManager) IsEnabled() bool { return c.config.GetBool("CACHE_ENABLED") } // CacheKey generates a cache key with prefix func CacheKey(prefix, key string) string { return prefix + ":" + key } // Common cache key prefixes const ( KeyPrefixPermission = "perm" KeyPrefixApplication = "app" KeyPrefixToken = "token" KeyPrefixUserClaims = "user_claims" KeyPrefixTokenRevoked = "token_revoked" )