tercul-backend/cache/redis_cache.go
Damir Mukimov 4957117cb6 Initial commit: Tercul Go project with comprehensive architecture
- Core Go application with GraphQL API using gqlgen
- Comprehensive data models for literary works, authors, translations
- Repository pattern with caching layer
- Authentication and authorization system
- Linguistics analysis capabilities with multiple adapters
- Vector search integration with Weaviate
- Docker containerization support
- Python data migration and analysis scripts
- Clean architecture with proper separation of concerns
- Production-ready configuration and middleware
- Proper .gitignore excluding vendor/, database files, and build artifacts
2025-08-13 07:42:32 +02:00

214 lines
5.5 KiB
Go

package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"tercul/config"
"tercul/logger"
)
// 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() (*RedisCache, error) {
// Create Redis client from config
client := redis.NewClient(&redis.Options{
Addr: config.Cfg.RedisAddr,
Password: config.Cfg.RedisPassword,
DB: config.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 {
logger.LogWarn("Invalid type in Redis cache",
logger.F("key", key),
logger.F("type", fmt.Sprintf("%T", values[i])))
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()
}