This commit is contained in:
2025-08-26 19:16:41 -04:00
parent 7ca61eb712
commit 6725529b01
113 changed files with 0 additions and 337 deletions

260
kms/internal/cache/cache.go vendored Normal file
View File

@ -0,0 +1,260 @@
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"
)

191
kms/internal/cache/redis.go vendored Normal file
View File

@ -0,0 +1,191 @@
package cache
import (
"context"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/config"
"github.com/kms/api-key-service/internal/errors"
)
// RedisCache implements CacheProvider using Redis
type RedisCache struct {
client *redis.Client
config config.ConfigProvider
logger *zap.Logger
}
// NewRedisCache creates a new Redis cache provider
func NewRedisCache(config config.ConfigProvider, logger *zap.Logger) (CacheProvider, error) {
// Redis configuration
redisAddr := config.GetString("REDIS_ADDR")
if redisAddr == "" {
redisAddr = "localhost:6379"
}
redisPassword := config.GetString("REDIS_PASSWORD")
redisDB := config.GetInt("REDIS_DB")
// Create Redis client
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: redisPassword,
DB: redisDB,
PoolSize: config.GetInt("REDIS_POOL_SIZE"),
MinIdleConns: config.GetInt("REDIS_MIN_IDLE_CONNS"),
MaxRetries: config.GetInt("REDIS_MAX_RETRIES"),
DialTimeout: config.GetDuration("REDIS_DIAL_TIMEOUT"),
ReadTimeout: config.GetDuration("REDIS_READ_TIMEOUT"),
WriteTimeout: config.GetDuration("REDIS_WRITE_TIMEOUT"),
})
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
logger.Error("Failed to connect to Redis", zap.Error(err))
return nil, errors.NewInternalError("Failed to connect to Redis").WithInternal(err)
}
logger.Info("Connected to Redis successfully", zap.String("addr", redisAddr))
return &RedisCache{
client: client,
config: config,
logger: logger,
}, nil
}
// Get retrieves a value from Redis cache
func (r *RedisCache) Get(ctx context.Context, key string) ([]byte, error) {
r.logger.Debug("Getting value from Redis cache", zap.String("key", key))
result, err := r.client.Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, errors.NewNotFoundError("cache key")
}
r.logger.Error("Failed to get value from Redis", zap.Error(err))
return nil, errors.NewInternalError("Failed to get cached value").WithInternal(err)
}
return []byte(result), nil
}
// Set stores a value in Redis cache with TTL
func (r *RedisCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
r.logger.Debug("Setting value in Redis cache",
zap.String("key", key),
zap.Duration("ttl", ttl))
err := r.client.Set(ctx, key, value, ttl).Err()
if err != nil {
r.logger.Error("Failed to set value in Redis", zap.Error(err))
return errors.NewInternalError("Failed to cache value").WithInternal(err)
}
return nil
}
// Delete removes a value from Redis cache
func (r *RedisCache) Delete(ctx context.Context, key string) error {
r.logger.Debug("Deleting value from Redis cache", zap.String("key", key))
err := r.client.Del(ctx, key).Err()
if err != nil {
r.logger.Error("Failed to delete value from Redis", zap.Error(err))
return errors.NewInternalError("Failed to delete cached value").WithInternal(err)
}
return nil
}
// Exists checks if a key exists in Redis cache
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
count, err := r.client.Exists(ctx, key).Result()
if err != nil {
r.logger.Error("Failed to check key existence in Redis", zap.Error(err))
return false, errors.NewInternalError("Failed to check cache key existence").WithInternal(err)
}
return count > 0, nil
}
// Clear removes all values from Redis cache (use with caution)
func (r *RedisCache) Clear(ctx context.Context) error {
r.logger.Warn("Clearing Redis cache - this will remove ALL cached data")
err := r.client.FlushDB(ctx).Err()
if err != nil {
r.logger.Error("Failed to clear Redis cache", zap.Error(err))
return errors.NewInternalError("Failed to clear cache").WithInternal(err)
}
return nil
}
// Close closes the Redis connection
func (r *RedisCache) Close() error {
r.logger.Info("Closing Redis connection")
return r.client.Close()
}
// SetNX sets a key only if it doesn't exist (Redis-specific operation)
func (r *RedisCache) SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error) {
r.logger.Debug("Setting value in Redis cache with NX",
zap.String("key", key),
zap.Duration("ttl", ttl))
result, err := r.client.SetNX(ctx, key, value, ttl).Result()
if err != nil {
r.logger.Error("Failed to set NX value in Redis", zap.Error(err))
return false, errors.NewInternalError("Failed to cache value with NX").WithInternal(err)
}
return result, nil
}
// Expire sets TTL for an existing key
func (r *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
r.logger.Debug("Setting TTL for Redis key",
zap.String("key", key),
zap.Duration("ttl", ttl))
result, err := r.client.Expire(ctx, key, ttl).Result()
if err != nil {
r.logger.Error("Failed to set TTL in Redis", zap.Error(err))
return errors.NewInternalError("Failed to set key TTL").WithInternal(err)
}
if !result {
return errors.NewNotFoundError("cache key")
}
return nil
}
// TTL returns the remaining time to live for a key
func (r *RedisCache) TTL(ctx context.Context, key string) (time.Duration, error) {
ttl, err := r.client.TTL(ctx, key).Result()
if err != nil {
r.logger.Error("Failed to get TTL from Redis", zap.Error(err))
return 0, errors.NewInternalError("Failed to get key TTL").WithInternal(err)
}
return ttl, nil
}
// Keys returns all keys matching a pattern
func (r *RedisCache) Keys(ctx context.Context, pattern string) ([]string, error) {
keys, err := r.client.Keys(ctx, pattern).Result()
if err != nil {
r.logger.Error("Failed to get keys from Redis", zap.Error(err))
return nil, errors.NewInternalError("Failed to get cache keys").WithInternal(err)
}
return keys, nil
}