Files
skybridge/internal/cache/cache.go
2025-08-22 15:01:40 -04:00

251 lines
6.3 KiB
Go

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
// For now, we'll use memory cache. In production, this could be Redis
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"
)