|
|
package common |
|
|
|
|
|
import ( |
|
|
"context" |
|
|
"errors" |
|
|
"fmt" |
|
|
"os" |
|
|
"reflect" |
|
|
"strconv" |
|
|
"time" |
|
|
|
|
|
"github.com/go-redis/redis/v8" |
|
|
"gorm.io/gorm" |
|
|
) |
|
|
|
|
|
var RDB *redis.Client |
|
|
var RedisEnabled = true |
|
|
|
|
|
func RedisKeyCacheSeconds() int { |
|
|
return SyncFrequency |
|
|
} |
|
|
|
|
|
|
|
|
func InitRedisClient() (err error) { |
|
|
if os.Getenv("REDIS_CONN_STRING") == "" { |
|
|
RedisEnabled = false |
|
|
SysLog("REDIS_CONN_STRING not set, Redis is not enabled") |
|
|
return nil |
|
|
} |
|
|
if os.Getenv("SYNC_FREQUENCY") == "" { |
|
|
SysLog("SYNC_FREQUENCY not set, use default value 60") |
|
|
SyncFrequency = 60 |
|
|
} |
|
|
SysLog("Redis is enabled") |
|
|
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) |
|
|
if err != nil { |
|
|
FatalLog("failed to parse Redis connection string: " + err.Error()) |
|
|
} |
|
|
opt.PoolSize = GetEnvOrDefault("REDIS_POOL_SIZE", 10) |
|
|
RDB = redis.NewClient(opt) |
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
|
defer cancel() |
|
|
|
|
|
_, err = RDB.Ping(ctx).Result() |
|
|
if err != nil { |
|
|
FatalLog("Redis ping test failed: " + err.Error()) |
|
|
} |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis connected to %s", opt.Addr)) |
|
|
SysLog(fmt.Sprintf("Redis database: %d", opt.DB)) |
|
|
} |
|
|
return err |
|
|
} |
|
|
|
|
|
func ParseRedisOption() *redis.Options { |
|
|
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) |
|
|
if err != nil { |
|
|
FatalLog("failed to parse Redis connection string: " + err.Error()) |
|
|
} |
|
|
return opt |
|
|
} |
|
|
|
|
|
func RedisSet(key string, value string, expiration time.Duration) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
return RDB.Set(ctx, key, value, expiration).Err() |
|
|
} |
|
|
|
|
|
func RedisGet(key string) (string, error) { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis GET: key=%s", key)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
val, err := RDB.Get(ctx, key).Result() |
|
|
return val, err |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func RedisDel(key string) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis DEL: key=%s", key)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
return RDB.Del(ctx, key).Err() |
|
|
} |
|
|
|
|
|
func RedisDelKey(key string) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
return RDB.Del(ctx, key).Err() |
|
|
} |
|
|
|
|
|
func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
|
|
|
data := make(map[string]interface{}) |
|
|
|
|
|
|
|
|
v := reflect.ValueOf(obj).Elem() |
|
|
t := v.Type() |
|
|
for i := 0; i < v.NumField(); i++ { |
|
|
field := t.Field(i) |
|
|
value := v.Field(i) |
|
|
|
|
|
|
|
|
if field.Type.String() == "gorm.DeletedAt" { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if value.Kind() == reflect.Ptr { |
|
|
if value.IsNil() { |
|
|
data[field.Name] = "" |
|
|
continue |
|
|
} |
|
|
value = value.Elem() |
|
|
} |
|
|
|
|
|
|
|
|
if value.Kind() == reflect.Bool { |
|
|
data[field.Name] = strconv.FormatBool(value.Bool()) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
data[field.Name] = fmt.Sprintf("%v", value.Interface()) |
|
|
} |
|
|
|
|
|
txn := RDB.TxPipeline() |
|
|
txn.HSet(ctx, key, data) |
|
|
|
|
|
|
|
|
if expiration > 0 { |
|
|
txn.Expire(ctx, key, expiration) |
|
|
} |
|
|
|
|
|
_, err := txn.Exec(ctx) |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to execute transaction: %w", err) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
func RedisHGetObj(key string, obj interface{}) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis HGETALL: key=%s", key)) |
|
|
} |
|
|
ctx := context.Background() |
|
|
|
|
|
result, err := RDB.HGetAll(ctx, key).Result() |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to load hash from Redis: %w", err) |
|
|
} |
|
|
|
|
|
if len(result) == 0 { |
|
|
return fmt.Errorf("key %s not found in Redis", key) |
|
|
} |
|
|
|
|
|
|
|
|
val := reflect.ValueOf(obj) |
|
|
if val.Kind() != reflect.Ptr { |
|
|
return fmt.Errorf("obj must be a pointer to a struct, got %T", obj) |
|
|
} |
|
|
|
|
|
v := val.Elem() |
|
|
if v.Kind() != reflect.Struct { |
|
|
return fmt.Errorf("obj must be a pointer to a struct, got pointer to %T", v.Interface()) |
|
|
} |
|
|
|
|
|
t := v.Type() |
|
|
for i := 0; i < v.NumField(); i++ { |
|
|
field := t.Field(i) |
|
|
fieldName := field.Name |
|
|
if value, ok := result[fieldName]; ok { |
|
|
fieldValue := v.Field(i) |
|
|
|
|
|
|
|
|
if fieldValue.Kind() == reflect.Ptr { |
|
|
if value == "" { |
|
|
continue |
|
|
} |
|
|
if fieldValue.IsNil() { |
|
|
fieldValue.Set(reflect.New(fieldValue.Type().Elem())) |
|
|
} |
|
|
fieldValue = fieldValue.Elem() |
|
|
} |
|
|
|
|
|
|
|
|
switch fieldValue.Kind() { |
|
|
case reflect.String: |
|
|
fieldValue.SetString(value) |
|
|
case reflect.Int, reflect.Int64: |
|
|
intValue, err := strconv.ParseInt(value, 10, 64) |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to parse int field %s: %w", fieldName, err) |
|
|
} |
|
|
fieldValue.SetInt(intValue) |
|
|
case reflect.Bool: |
|
|
boolValue, err := strconv.ParseBool(value) |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to parse bool field %s: %w", fieldName, err) |
|
|
} |
|
|
fieldValue.SetBool(boolValue) |
|
|
case reflect.Struct: |
|
|
|
|
|
if fieldValue.Type().String() == "gorm.DeletedAt" { |
|
|
if value != "" { |
|
|
timeValue, err := time.Parse(time.RFC3339, value) |
|
|
if err != nil { |
|
|
return fmt.Errorf("failed to parse DeletedAt field %s: %w", fieldName, err) |
|
|
} |
|
|
fieldValue.Set(reflect.ValueOf(gorm.DeletedAt{Time: timeValue, Valid: true})) |
|
|
} |
|
|
} |
|
|
default: |
|
|
return fmt.Errorf("unsupported field type: %s for field %s", fieldValue.Kind(), fieldName) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func RedisIncr(key string, delta int64) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis INCR: key=%s, delta=%d", key, delta)) |
|
|
} |
|
|
|
|
|
ttlCmd := RDB.TTL(context.Background(), key) |
|
|
ttl, err := ttlCmd.Result() |
|
|
if err != nil && !errors.Is(err, redis.Nil) { |
|
|
return fmt.Errorf("failed to get TTL: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
if ttl > 0 { |
|
|
ctx := context.Background() |
|
|
|
|
|
txn := RDB.TxPipeline() |
|
|
|
|
|
|
|
|
decrCmd := txn.IncrBy(ctx, key, delta) |
|
|
if err := decrCmd.Err(); err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
|
|
|
txn.Expire(ctx, key, ttl) |
|
|
|
|
|
|
|
|
_, err = txn.Exec(ctx) |
|
|
return err |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
func RedisHIncrBy(key, field string, delta int64) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta)) |
|
|
} |
|
|
ttlCmd := RDB.TTL(context.Background(), key) |
|
|
ttl, err := ttlCmd.Result() |
|
|
if err != nil && !errors.Is(err, redis.Nil) { |
|
|
return fmt.Errorf("failed to get TTL: %w", err) |
|
|
} |
|
|
|
|
|
if ttl > 0 { |
|
|
ctx := context.Background() |
|
|
txn := RDB.TxPipeline() |
|
|
|
|
|
incrCmd := txn.HIncrBy(ctx, key, field, delta) |
|
|
if err := incrCmd.Err(); err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
txn.Expire(ctx, key, ttl) |
|
|
|
|
|
_, err = txn.Exec(ctx) |
|
|
return err |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
func RedisHSetField(key, field string, value interface{}) error { |
|
|
if DebugEnabled { |
|
|
SysLog(fmt.Sprintf("Redis HSET field: key=%s, field=%s, value=%v", key, field, value)) |
|
|
} |
|
|
ttlCmd := RDB.TTL(context.Background(), key) |
|
|
ttl, err := ttlCmd.Result() |
|
|
if err != nil && !errors.Is(err, redis.Nil) { |
|
|
return fmt.Errorf("failed to get TTL: %w", err) |
|
|
} |
|
|
|
|
|
if ttl > 0 { |
|
|
ctx := context.Background() |
|
|
txn := RDB.TxPipeline() |
|
|
|
|
|
hsetCmd := txn.HSet(ctx, key, field, value) |
|
|
if err := hsetCmd.Err(); err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
txn.Expire(ctx, key, ttl) |
|
|
|
|
|
_, err = txn.Exec(ctx) |
|
|
return err |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|