org
This commit is contained in:
260
kms/internal/cache/cache.go
vendored
Normal file
260
kms/internal/cache/cache.go
vendored
Normal 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
191
kms/internal/cache/redis.go
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user