package redis import ( "context" "encoding/json" "fmt" "math" "os" "strings" "time" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs" "github.com/go-redis/redis_rate/v9" "github.com/go-redis/redis/v8" ) var ( ctx = context.Background() redisClient *ClientWithHelpers ) type ClientWithHelpers struct { Client *redis.Client Available bool } func GetRedisClient(isDebug bool) *ClientWithHelpers { if redisClient != nil && redisClient.IsConnected() { return redisClient } redisClient = connectToRedis(isDebug) return redisClient } func connectToRedis(isDebug bool) *ClientWithHelpers { if os.Getenv("REDIS_HOST") != "false" { host := os.Getenv("REDIS_HOST") port := os.Getenv("REDIS_PORT") if isDebug { env := os.Getenv("ENVIRONMENT") switch env { case "dev": port = "6380" case "stage": port = "6381" case "sandbox", "qa": port = "6382" case "prod": port = "6383" } } return NewClient(host + ":" + port) } return &ClientWithHelpers{ Client: nil, Available: false, } } func NewClient(addr string) *ClientWithHelpers { return &ClientWithHelpers{ Client: redis.NewClient(&redis.Options{ MaxRetries: 1, DialTimeout: time.Duration(1) * time.Second, // So max 2 second wait Addr: addr, Password: "", // no password set DB: 0, // use default Db }), Available: true, } } func (r ClientWithHelpers) IsConnected() bool { return r.Client != nil && r.Available == true } func (r ClientWithHelpers) DeleteByKey(key string) error { if !r.IsConnected() { return errors.Errorf("REDIS disabled: cannot del key(%s)", key) } _, err := r.Client.Del(ctx, key).Result() if err != nil { return errors.Wrapf(err, "failed to del key(%s)", key) } return nil } func (r ClientWithHelpers) DeleteByKeyPattern(pattern string) { if !r.IsConnected() { return } iter := r.Client.Scan(ctx, 0, pattern, math.MaxInt64).Iterator() for iter.Next(ctx) { val := iter.Val() err := r.Client.Del(ctx, val).Err() if err != nil { panic(err) } } if err := iter.Err(); err != nil { panic(err) } } func (r ClientWithHelpers) SetObjectByKey(key string, object interface{}) { if !r.IsConnected() { return } jsonBytes, err := json.Marshal(object) if err != nil { logs.ErrorWithMsg("Error marshalling object to Redis: %s", err) return } _, err = r.Client.Set(ctx, key, string(jsonBytes), 24*time.Hour).Result() if err != nil { logs.ErrorWithMsg(fmt.Sprintf("Error setting value to Redis for key: %s", key), err) /* Prevent further calls in this execution from trying to connect and also timeout */ if strings.HasSuffix(err.Error(), "i/o timeout") { r.Available = false } } } func (r ClientWithHelpers) SetObjectByKeyWithExpiry(key string, object interface{}, expiration time.Duration) { if !r.IsConnected() { return } jsonBytes, err := json.Marshal(object) if err != nil { logs.ErrorWithMsg("Error marshalling object to Redis: %s", err) return } _, err = r.Client.Set(ctx, key, string(jsonBytes), expiration).Result() if err != nil { logs.ErrorWithMsg(fmt.Sprintf("Error setting value to Redis for key: %s", key), err) /* Prevent further calls in this execution from trying to connect and also timeout */ if strings.HasSuffix(err.Error(), "i/o timeout") { r.Available = false } } } func (r ClientWithHelpers) SetObjectByKeyIndefinitely(key string, object interface{}) { if !r.IsConnected() { return } jsonBytes, err := json.Marshal(object) if err != nil { logs.ErrorWithMsg("Error marshalling object to Redis", err) return } _, err = r.Client.Set(ctx, key, string(jsonBytes), 0).Result() if err != nil { logs.ErrorWithMsg(fmt.Sprintf("Error setting value to Redis for key: %s", key), err) /* Prevent further calls in this execution from trying to connect and also timeout */ if strings.HasSuffix(err.Error(), "i/o timeout") { r.Available = false } } } func (r ClientWithHelpers) GetValueByKey(key string) string { if !r.IsConnected() { return "" } jsonString, err := r.Client.Get(ctx, key).Result() if err == redis.Nil { /* Key does not exist */ return "" } else if err != nil { /* Actual error */ logs.Warn(fmt.Sprintf("Error fetching object from Redis for key: %s", key), err) /* Prevent further calls in this execution from trying to connect and also timeout */ if strings.HasSuffix(err.Error(), "i/o timeout") { r.Available = false } } return jsonString } func (r ClientWithHelpers) RateLimit(key string, limitFn func(int) redis_rate.Limit, limit int) (bool, error) { limiter := redis_rate.NewLimiter(r.Client) res, err := limiter.Allow(ctx, key, limitFn(limit)) if err != nil { logs.ErrorWithMsg(fmt.Sprintf("Redis Error rate limiting - %s", key), err) /* Prevent further calls in this execution from trying to connect and also timeout */ if strings.HasSuffix(err.Error(), "i/o timeout") { r.Available = false } return false, err } else { return res.Allowed == 1, nil } }