package redis import ( "context" "fmt" "time" "git.intra.yksa.space/gsn/predictor/internal/pkg/errcodes" "github.com/bsm/redislock" "github.com/redis/go-redis/v9" ) type Client struct { client *redis.Client locker *redislock.Client } // Ensure Client implements Service interface var _ Service = (*Client)(nil) type Config struct { Host string `env:"HOST"` Port int `env:"PORT"` Password string `env:"PASSWORD"` DB int `env:"DB"` } func New(cfg Config) (*Client, error) { client := redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), Password: cfg.Password, DB: cfg.DB, }) // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { return nil, fmt.Errorf("failed to connect to redis: %w", err) } locker := redislock.New(client) return &Client{ client: client, locker: locker, }, nil } func (c *Client) Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error) { lock, err := c.locker.Obtain(ctx, key, ttl, nil) if err != nil { if err == redislock.ErrNotObtained { return nil, errcodes.ErrRedisLockAlreadyLocked } return nil, errcodes.Wrap(err, "failed to obtain redis lock") } unlock := func(ctx context.Context) { lock.Release(ctx) } return unlock, nil } func (c *Client) Set(key string, value []byte, ttl time.Duration) error { ctx := context.Background() if err := c.client.Set(ctx, key, value, ttl).Err(); err != nil { return errcodes.Wrap(err, "failed to set redis key") } return nil } func (c *Client) Get(key string) ([]byte, error) { ctx := context.Background() result := c.client.Get(ctx, key) if result.Err() != nil { if result.Err() == redis.Nil { return nil, errcodes.ErrRedisCacheMiss } return nil, errcodes.Wrap(result.Err(), "failed to get redis key") } return result.Bytes() } func (c *Client) Close() error { return c.client.Close() }