package cache import ( "context" "encoding/json" "errors" "strings" "time" redis "github.com/redis/go-redis/v9" ) // L2Cache defines the distributed cache contract. type L2Cache interface { Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error Get(ctx context.Context, key string) (interface{}, error) Delete(ctx context.Context, key string) error Exists(ctx context.Context, key string) (bool, error) Clear(ctx context.Context) error Close() error } // RedisCacheConfig configures the Redis-backed L2 cache. type RedisCacheConfig struct { Enabled bool Addr string Password string DB int PoolSize int } // RedisCache implements L2Cache using Redis. type RedisCache struct { enabled bool client *redis.Client } // NewRedisCache keeps the old test-friendly constructor. func NewRedisCache(enabled bool) *RedisCache { return NewRedisCacheWithConfig(RedisCacheConfig{Enabled: enabled}) } // NewRedisCacheWithConfig creates a Redis-backed L2 cache. func NewRedisCacheWithConfig(cfg RedisCacheConfig) *RedisCache { cache := &RedisCache{enabled: cfg.Enabled} if !cfg.Enabled { return cache } addr := cfg.Addr if addr == "" { addr = "localhost:6379" } options := &redis.Options{ Addr: addr, Password: cfg.Password, DB: cfg.DB, } if cfg.PoolSize > 0 { options.PoolSize = cfg.PoolSize } cache.client = redis.NewClient(options) return cache } func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { if !c.enabled || c.client == nil { return nil } payload, err := json.Marshal(value) if err != nil { return err } return c.client.Set(ctx, key, payload, ttl).Err() } func (c *RedisCache) Get(ctx context.Context, key string) (interface{}, error) { if !c.enabled || c.client == nil { return nil, nil } raw, err := c.client.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { return nil, nil } if err != nil { return nil, err } return decodeRedisValue(raw) } func (c *RedisCache) Delete(ctx context.Context, key string) error { if !c.enabled || c.client == nil { return nil } return c.client.Del(ctx, key).Err() } func (c *RedisCache) Exists(ctx context.Context, key string) (bool, error) { if !c.enabled || c.client == nil { return false, nil } count, err := c.client.Exists(ctx, key).Result() if err != nil { return false, err } return count > 0, nil } func (c *RedisCache) Clear(ctx context.Context) error { if !c.enabled || c.client == nil { return nil } return c.client.FlushDB(ctx).Err() } func (c *RedisCache) Close() error { if !c.enabled || c.client == nil { return nil } return c.client.Close() } func decodeRedisValue(raw string) (interface{}, error) { decoder := json.NewDecoder(strings.NewReader(raw)) decoder.UseNumber() var value interface{} if err := decoder.Decode(&value); err != nil { return raw, nil } return normalizeRedisValue(value), nil } func normalizeRedisValue(value interface{}) interface{} { switch v := value.(type) { case json.Number: if n, err := v.Int64(); err == nil { return n } if n, err := v.Float64(); err == nil { return n } return v.String() case []interface{}: for i := range v { v[i] = normalizeRedisValue(v[i]) } return v case map[string]interface{}: for key, item := range v { v[key] = normalizeRedisValue(item) } return v default: return v } }