package cache import ( "context" "encoding/json" "errors" "fmt" "tercul/internal/platform/config" "tercul/internal/platform/log" "time" "github.com/redis/go-redis/v9" ) // RedisCache implements the Cache interface using Redis type RedisCache struct { client *redis.Client keyGenerator KeyGenerator defaultExpiry time.Duration } // NewRedisCache creates a new RedisCache func NewRedisCache(client *redis.Client, keyGenerator KeyGenerator, defaultExpiry time.Duration) *RedisCache { if keyGenerator == nil { keyGenerator = NewDefaultKeyGenerator("") } if defaultExpiry == 0 { defaultExpiry = 1 * time.Hour // Default expiry of 1 hour } return &RedisCache{ client: client, keyGenerator: keyGenerator, defaultExpiry: defaultExpiry, } } // NewDefaultRedisCache creates a new RedisCache with default settings func NewDefaultRedisCache(cfg *config.Config) (*RedisCache, error) { // Create Redis client from config client := redis.NewClient(&redis.Options{ Addr: cfg.RedisAddr, Password: cfg.RedisPassword, DB: cfg.RedisDB, }) // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { return nil, err } return NewRedisCache(client, nil, 0), nil } // Get retrieves a value from the cache func (c *RedisCache) Get(ctx context.Context, key string, value interface{}) error { data, err := c.client.Get(ctx, key).Bytes() if err != nil { if err == redis.Nil { return errors.New("cache miss") } return err } return json.Unmarshal(data, value) } // Set stores a value in the cache with an optional expiration func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { data, err := json.Marshal(value) if err != nil { return err } if expiration == 0 { expiration = c.defaultExpiry } return c.client.Set(ctx, key, data, expiration).Err() } // Delete removes a value from the cache func (c *RedisCache) Delete(ctx context.Context, key string) error { return c.client.Del(ctx, key).Err() } // Clear removes all values from the cache func (c *RedisCache) Clear(ctx context.Context) error { return c.client.FlushAll(ctx).Err() } // GetMulti retrieves multiple values from the cache func (c *RedisCache) GetMulti(ctx context.Context, keys []string) (map[string][]byte, error) { if len(keys) == 0 { return make(map[string][]byte), nil } values, err := c.client.MGet(ctx, keys...).Result() if err != nil { return nil, err } result := make(map[string][]byte, len(keys)) for i, key := range keys { if values[i] == nil { continue } str, ok := values[i].(string) if !ok { log.FromContext(ctx).With("key", key).With("type", fmt.Sprintf("%T", values[i])).Warn("Invalid type in Redis cache") continue } result[key] = []byte(str) } return result, nil } // SetMulti stores multiple values in the cache with an optional expiration func (c *RedisCache) SetMulti(ctx context.Context, items map[string]interface{}, expiration time.Duration) error { if len(items) == 0 { return nil } if expiration == 0 { expiration = c.defaultExpiry } pipe := c.client.Pipeline() for key, value := range items { data, err := json.Marshal(value) if err != nil { return err } pipe.Set(ctx, key, data, expiration) } _, err := pipe.Exec(ctx) return err } // GetEntity retrieves an entity by ID from the cache func (c *RedisCache) GetEntity(ctx context.Context, entityType string, id uint, value interface{}) error { key := c.keyGenerator.EntityKey(entityType, id) return c.Get(ctx, key, value) } // SetEntity stores an entity in the cache func (c *RedisCache) SetEntity(ctx context.Context, entityType string, id uint, value interface{}, expiration time.Duration) error { key := c.keyGenerator.EntityKey(entityType, id) return c.Set(ctx, key, value, expiration) } // DeleteEntity removes an entity from the cache func (c *RedisCache) DeleteEntity(ctx context.Context, entityType string, id uint) error { key := c.keyGenerator.EntityKey(entityType, id) return c.Delete(ctx, key) } // GetList retrieves a list of entities from the cache func (c *RedisCache) GetList(ctx context.Context, entityType string, page, pageSize int, value interface{}) error { key := c.keyGenerator.ListKey(entityType, page, pageSize) return c.Get(ctx, key, value) } // SetList stores a list of entities in the cache func (c *RedisCache) SetList(ctx context.Context, entityType string, page, pageSize int, value interface{}, expiration time.Duration) error { key := c.keyGenerator.ListKey(entityType, page, pageSize) return c.Set(ctx, key, value, expiration) } // DeleteList removes a list of entities from the cache func (c *RedisCache) DeleteList(ctx context.Context, entityType string, page, pageSize int) error { key := c.keyGenerator.ListKey(entityType, page, pageSize) return c.Delete(ctx, key) } // InvalidateEntityType removes all cached data for a specific entity type func (c *RedisCache) InvalidateEntityType(ctx context.Context, entityType string) error { pattern := c.keyGenerator.(*DefaultKeyGenerator).Prefix + entityType + ":*" // Use SCAN to find all keys matching the pattern iter := c.client.Scan(ctx, 0, pattern, 100).Iterator() var keys []string for iter.Next(ctx) { keys = append(keys, iter.Val()) // Delete in batches of 100 to avoid large operations if len(keys) >= 100 { if err := c.client.Del(ctx, keys...).Err(); err != nil { return err } keys = keys[:0] } } // Delete any remaining keys if len(keys) > 0 { return c.client.Del(ctx, keys...).Err() } return iter.Err() }