package cache import ( "context" "errors" "fmt" "strings" "time" "github.com/redis/go-redis/v9" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config" ) // silentRedisLogger filters out noisy "maintnotifications" warnings from go-redis // This warning occurs when the Redis client tries to use newer Redis 7.2+ features // that may not be fully supported by the current Redis version. // The client automatically falls back to compatible mode, so this is harmless. type silentRedisLogger struct { logger *zap.Logger } func (l *silentRedisLogger) Printf(ctx context.Context, format string, v ...interface{}) { msg := fmt.Sprintf(format, v...) // Filter out harmless compatibility warnings if strings.Contains(msg, "maintnotifications disabled") || strings.Contains(msg, "auto mode fallback") { return } // Log other Redis messages at debug level l.logger.Debug(msg) } // RedisCacher defines the interface for Redis cache operations type RedisCacher interface { Shutdown(ctx context.Context) Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, val []byte) error SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error Delete(ctx context.Context, key string) error } type redisCache struct { client *redis.Client logger *zap.Logger } // NewRedisCache creates a new Redis cache instance func NewRedisCache(cfg *config.Config, logger *zap.Logger) (RedisCacher, error) { logger = logger.Named("redis-cache") logger.Info("⏳ Connecting to Redis...", zap.String("host", cfg.Cache.Host), zap.Int("port", cfg.Cache.Port)) // Build Redis URL from config redisURL := fmt.Sprintf("redis://:%s@%s:%d/%d", cfg.Cache.Password, cfg.Cache.Host, cfg.Cache.Port, cfg.Cache.DB, ) // If no password, use simpler URL format if cfg.Cache.Password == "" { redisURL = fmt.Sprintf("redis://%s:%d/%d", cfg.Cache.Host, cfg.Cache.Port, cfg.Cache.DB, ) } opt, err := redis.ParseURL(redisURL) if err != nil { return nil, fmt.Errorf("failed to parse Redis URL: %w", err) } // Suppress noisy "maintnotifications" warnings from go-redis // Use a custom logger that filters out these harmless compatibility warnings redis.SetLogger(&silentRedisLogger{logger: logger.Named("redis-client")}) client := redis.NewClient(opt) // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if _, err = client.Ping(ctx).Result(); err != nil { return nil, fmt.Errorf("failed to connect to Redis: %w", err) } logger.Info("✓ Redis connected", zap.String("host", cfg.Cache.Host), zap.Int("port", cfg.Cache.Port), zap.Int("db", cfg.Cache.DB)) return &redisCache{ client: client, logger: logger, }, nil } func (c *redisCache) Shutdown(ctx context.Context) { c.logger.Info("shutting down Redis cache") if err := c.client.Close(); err != nil { c.logger.Error("error closing Redis connection", zap.Error(err)) } } func (c *redisCache) Get(ctx context.Context, key string) ([]byte, error) { val, err := c.client.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { // Key doesn't exist - this is not an error return nil, nil } if err != nil { return nil, fmt.Errorf("redis get failed: %w", err) } return []byte(val), nil } func (c *redisCache) Set(ctx context.Context, key string, val []byte) error { if err := c.client.Set(ctx, key, val, 0).Err(); err != nil { return fmt.Errorf("redis set failed: %w", err) } return nil } func (c *redisCache) SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error { if err := c.client.Set(ctx, key, val, expiry).Err(); err != nil { return fmt.Errorf("redis set with expiry failed: %w", err) } return nil } func (c *redisCache) Delete(ctx context.Context, key string) error { if err := c.client.Del(ctx, key).Err(); err != nil { return fmt.Errorf("redis delete failed: %w", err) } return nil }