package db import ( "context" "log" "strings" "time" "github.com/redis/go-redis/v9" ) // RedisWrapper wraps go-redis client to implement RedisClient interface. type RedisWrapper struct { client *redis.Client } // NewRedisWrapper creates a RedisWrapper from a go-redis client. func NewRedisWrapper(client *redis.Client) *RedisWrapper { return &RedisWrapper{client: client} } // InitRedis initializes Redis for presence, rate limiting, and pub/sub (ADR-007). // Returns nil if Redis is unavailable (non-fatal). func InitRedis(redisAddress string) *RedisWrapper { var options *redis.Options var err error if strings.Contains(redisAddress, "://") { options, err = redis.ParseURL(redisAddress) if err != nil { log.Printf("Redis configuration warning: %v (rate limiting and presence will be disabled)", err) return nil } } else { options = &redis.Options{Addr: redisAddress} } client := redis.NewClient(options) if err := client.Ping(context.Background()).Err(); err != nil { log.Printf("Redis connection warning: %v (rate limiting and presence will be disabled)", err) client.Close() // Start auto-reconnect goroutine go redisReconnectLoop(redisAddress, options) return nil } log.Println("Redis connected successfully (presence, rate limiting, pub/sub enabled)") return &RedisWrapper{client: client} } // redisReconnectLoop attempts to reconnect to Redis with exponential backoff. func redisReconnectLoop(addr string, baseOptions *redis.Options) { backoff := 2 * time.Second maxBackoff := 60 * time.Second for { time.Sleep(backoff) testClient := redis.NewClient(baseOptions) if err := testClient.Ping(context.Background()).Err(); err != nil { log.Printf("Redis reconnect failed: %v (retry in %v)", err, backoff) backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } testClient.Close() continue } // Reconnected — caller must replace their reference. // This is a best-effort background reconnect; the wrapper // returned by InitRedis remains nil until a new InitRedis call. log.Println("Redis reconnected successfully (background)") testClient.Close() return } } // Ping checks Redis connectivity. func (r *RedisWrapper) Ping(ctx context.Context) error { if r == nil || r.client == nil { return redis.ErrClosed } return r.client.Ping(ctx).Err() } // Set stores a key with TTL. func (r *RedisWrapper) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { return r.client.Set(ctx, key, value, expiration).Err() } // Get retrieves a key. func (r *RedisWrapper) Get(ctx context.Context, key string) (string, error) { return r.client.Get(ctx, key).Result() } // Del removes keys. func (r *RedisWrapper) Del(ctx context.Context, keys ...string) error { return r.client.Del(ctx, keys...).Err() } // Incr increments a key. func (r *RedisWrapper) Incr(ctx context.Context, key string) (int64, error) { return r.client.Incr(ctx, key).Result() } // Decr decrements a key. func (r *RedisWrapper) Decr(ctx context.Context, key string) (int64, error) { return r.client.Decr(ctx, key).Result() } // Expire sets TTL on a key. func (r *RedisWrapper) Expire(ctx context.Context, key string, expiration time.Duration) error { return r.client.Expire(ctx, key, expiration).Err() } // Exists checks if keys exist. func (r *RedisWrapper) Exists(ctx context.Context, keys ...string) (int64, error) { return r.client.Exists(ctx, keys...).Result() } // Publish publishes a message to a channel. func (r *RedisWrapper) Publish(ctx context.Context, channel string, message interface{}) error { return r.client.Publish(ctx, channel, message).Err() } // PSubscribe subscribes to channels by pattern. func (r *RedisWrapper) PSubscribe(ctx context.Context, channels ...string) PubSub { ps := r.client.PSubscribe(ctx, channels...) return &redisPubSubWrapper{ps: ps} } // Scan scans keys matching a pattern. func (r *RedisWrapper) Scan(ctx context.Context, cursor uint64, match string, count int64) ([]string, uint64, error) { return r.client.Scan(ctx, cursor, match, count).Result() } // Info returns Redis server info. func (r *RedisWrapper) Info(ctx context.Context, sections ...string) (string, error) { return r.client.Info(ctx, sections...).Result() } // Close closes the Redis connection. func (r *RedisWrapper) Close() error { if r == nil || r.client == nil { return nil } return r.client.Close() } // redisPubSubWrapper wraps go-redis PubSub. type redisPubSubWrapper struct { ps *redis.PubSub } func (w *redisPubSubWrapper) ReceiveMessage(ctx context.Context) (*PubSubMessage, error) { msg, err := w.ps.ReceiveMessage(ctx) if err != nil { return nil, err } return &PubSubMessage{Channel: msg.Channel, Payload: msg.Payload}, nil } func (w *redisPubSubWrapper) Close() error { return w.ps.Close() }