chrly/db/redis.go
2019-04-25 02:23:10 +03:00

293 lines
6.5 KiB
Go

package db
import (
"bytes"
"compress/zlib"
"encoding/json"
"fmt"
"io"
"log"
"strconv"
"strings"
"time"
"github.com/mediocregopher/radix.v2/pool"
"github.com/mediocregopher/radix.v2/redis"
"github.com/mediocregopher/radix.v2/util"
"github.com/elyby/chrly/api/mojang/queue"
"github.com/elyby/chrly/interfaces"
"github.com/elyby/chrly/model"
)
type RedisFactory struct {
Host string
Port int
PoolSize int
connection *pool.Pool
}
// TODO: maybe we should manually return connection to the pool?
// TODO: Why isn't a pointer used here?
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
return f.createInstance()
}
func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
panic("capes repository not supported for this storage type")
}
func (f RedisFactory) CreateMojangUuidsRepository() (queue.UuidsStorage, error) {
return f.createInstance()
}
func (f RedisFactory) createInstance() (*redisDb, error) {
connection, err := f.getConnection()
if err != nil {
return nil, err
}
return &redisDb{connection}, nil
}
func (f RedisFactory) getConnection() (*pool.Pool, error) {
if f.connection == nil {
if f.Host == "" {
return nil, &ParamRequired{"host"}
}
if f.Port == 0 {
return nil, &ParamRequired{"port"}
}
addr := fmt.Sprintf("%s:%d", f.Host, f.Port)
conn, err := pool.New("tcp", addr, f.PoolSize)
if err != nil {
return nil, err
}
f.connection = conn
go func() {
period := 5
for {
time.Sleep(time.Duration(period) * time.Second)
resp := f.connection.Cmd("PING")
if resp.Err == nil {
continue
}
log.Println("Redis not pinged. Try to reconnect")
conn, err := pool.New("tcp", addr, f.PoolSize)
if err != nil {
log.Printf("Cannot reconnect to redis: %v\n", err)
log.Printf("Waiting %d seconds to retry\n", period)
continue
}
f.connection = conn
log.Println("Reconnected")
}
}()
}
return f.connection, nil
}
type redisDb struct {
conn *pool.Pool
}
const accountIdToUsernameKey = "hash:username-to-account-id"
const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid"
// TODO: return connection to the pool after usage
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
return findByUsername(username, db.getConn())
}
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
return findByUserId(id, db.getConn())
}
func (db *redisDb) Save(skin *model.Skin) error {
return save(skin, db.getConn())
}
func (db *redisDb) RemoveByUserId(id int) error {
return removeByUserId(id, db.getConn())
}
func (db *redisDb) RemoveByUsername(username string) error {
return removeByUsername(username, db.getConn())
}
func (db *redisDb) GetUuid(username string) (string, error) {
return findMojangUuidByUsername(username, db.getConn())
}
func (db *redisDb) StoreUuid(username string, uuid string) {
storeMojangUuid(username, uuid, db.getConn())
}
func (db *redisDb) getConn() util.Cmder {
conn, _ := db.conn.Get()
return conn
}
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
if username == "" {
return nil, &SkinNotFoundError{username}
}
redisKey := buildUsernameKey(username)
response := conn.Cmd("GET", redisKey)
if response.IsType(redis.Nil) {
return nil, &SkinNotFoundError{username}
}
encodedResult, err := response.Bytes()
if err != nil {
return nil, err
}
result, err := zlibDecode(encodedResult)
if err != nil {
return nil, err
}
var skin *model.Skin
err = json.Unmarshal(result, &skin)
if err != nil {
return nil, err
}
skin.OldUsername = skin.Username
return skin, nil
}
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
if response.IsType(redis.Nil) {
return nil, &SkinNotFoundError{"unknown"}
}
username, _ := response.Str()
return findByUsername(username, conn)
}
func removeByUserId(id int, conn util.Cmder) error {
record, err := findByUserId(id, conn)
if err != nil {
if _, ok := err.(*SkinNotFoundError); !ok {
return err
}
}
conn.Cmd("MULTI")
conn.Cmd("HDEL", accountIdToUsernameKey, id)
if record != nil {
conn.Cmd("DEL", buildUsernameKey(record.Username))
}
conn.Cmd("EXEC")
return nil
}
func removeByUsername(username string, conn util.Cmder) error {
record, err := findByUsername(username, conn)
if err != nil {
if _, ok := err.(*SkinNotFoundError); !ok {
return err
}
}
conn.Cmd("MULTI")
conn.Cmd("DEL", buildUsernameKey(record.Username))
if record != nil {
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
}
conn.Cmd("EXEC")
return nil
}
func save(skin *model.Skin, conn util.Cmder) error {
conn.Cmd("MULTI")
// If user has changed username, then we must delete his old username record
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
conn.Cmd("DEL", buildUsernameKey(skin.OldUsername))
}
// If this is a new record or if the user has changed username, we set the value in the hash table
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
}
str, _ := json.Marshal(skin)
conn.Cmd("SET", buildUsernameKey(skin.Username), zlibEncode(str))
conn.Cmd("EXEC")
skin.OldUsername = skin.Username
return nil
}
func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) {
response := conn.Cmd("HGET", mojangUsernameToUuidKey, strings.ToLower(username))
if response.IsType(redis.Nil) {
return "", &queue.ValueNotFound{}
}
data, _ := response.Str()
parts := strings.Split(data, ":")
timestamp, _ := strconv.ParseInt(parts[1], 10, 64)
storedAt := time.Unix(timestamp, 0)
if storedAt.Add(time.Hour * 24 * 30).Before(time.Now()) {
return "", &queue.ValueNotFound{}
}
return parts[0], nil
}
func storeMojangUuid(username string, uuid string, conn util.Cmder) {
value := uuid + ":" + strconv.FormatInt(time.Now().Unix(), 10)
conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value)
}
func buildUsernameKey(username string) string {
return "username:" + strings.ToLower(username)
}
func zlibEncode(str []byte) []byte {
var buff bytes.Buffer
writer := zlib.NewWriter(&buff)
writer.Write(str)
writer.Close()
return buff.Bytes()
}
func zlibDecode(bts []byte) ([]byte, error) {
buff := bytes.NewReader(bts)
reader, readError := zlib.NewReader(buff)
if readError != nil {
return nil, readError
}
resultBuffer := new(bytes.Buffer)
io.Copy(resultBuffer, reader)
reader.Close()
return resultBuffer.Bytes(), nil
}