Files
user-system/internal/cache/l2.go

166 lines
3.4 KiB
Go
Raw Normal View History

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
}
}