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 }