package cache import ( "context" "errors" "fmt" "github.com/redis/go-redis/v9" "log" "strconv" "time" ) var ctx = context.Background() type PlayerCache struct { cache *redis.Client lifetime time.Duration expireSub *redis.PubSub expireCallback func(key string) } func NewPlayerCache(address string, lifetime time.Duration, expireCallback func(key string)) *PlayerCache { pc := &PlayerCache{ redis.NewClient( &redis.Options{ Addr: address, Password: "", DB: 0, }), lifetime, nil, expireCallback, } pc.expireSub = pc.cache.PSubscribe(ctx, "__keyevent@0__:expired") go func() { for { msg, err := pc.expireSub.ReceiveMessage(ctx) if err != nil { log.Fatal(err) } else { pc.expireCallback(msg.Payload) } } }() return pc } func (pc *PlayerCache) Connect() error { _, err := pc.cache.Ping(ctx).Result() return err } func (pc *PlayerCache) GetValue(key string) (string, error) { val, err := pc.cache.Get(ctx, key).Result() if err != nil { return "", err // cache miss or error } else { return val, nil // cache hit } } func (pc *PlayerCache) SetValue(key string, value interface{}, lifetime time.Duration) error { return pc.cache.Set(ctx, key, value, lifetime).Err() } func (pc *PlayerCache) SetScore(playerId uint, gameTag string, score float32) error { key := getPlayerCacheKey(playerId, gameTag) return pc.SetValue(key, score, pc.lifetime) } func (pc *PlayerCache) GetScore(playerId uint, gameTag string) (float32, error) { key := getPlayerCacheKey(playerId, gameTag) val, err := pc.GetValue(key) if errors.Is(err, redis.Nil) { return -1.0, nil // cache miss } else if err != nil { return -1.0, err // cache error } else { valFloat, _ := strconv.ParseFloat(val, 32) return float32(valFloat), nil // cache hit } } func (pc *PlayerCache) GetScores(playerIds []uint, gameTag string) ([]float32, error) { vals, err := pc.cache.Pipelined(ctx, func(pipe redis.Pipeliner) error { for _, id := range playerIds { key := getPlayerCacheKey(id, gameTag) pipe.Get(ctx, key) } return nil }) if err != nil && !errors.Is(err, redis.Nil) { return nil, err } var scores []float32 for _, val := range vals { score, err := val.(*redis.StringCmd).Float32() if errors.Is(err, redis.Nil) { // cache miss score = -1 err = nil } else if err != nil { // cache error score = -1 } scores = append(scores, score) } return scores, nil } func (pc *PlayerCache) DeleteScore(playerId uint, gameTag string) error { key := getPlayerCacheKey(playerId, gameTag) return pc.cache.Del(ctx, key).Err() } func (pc *PlayerCache) PurgeCache() error { return pc.cache.FlushAll(ctx).Err() } func getPlayerCacheKey(playerId uint, gameTag string) string { return fmt.Sprintf("player:%d:game:%s", playerId, gameTag) } func (pc *PlayerCache) GetPlayerIdFromCacheKey(key string) (uint, error) { var playerId uint _, err := fmt.Sscanf(key, "player:%d:game:", &playerId) return playerId, err }