Add distributed locks to specific methods

This commit is contained in:
Jacob Gunther
2023-07-22 01:29:19 -05:00
parent 34c3156cc5
commit 55fd9f7bb4
6 changed files with 214 additions and 29 deletions

View File

@@ -77,6 +77,7 @@ var (
Cache: CacheConfig{
SkinCacheDuration: PointerOf(time.Hour * 12),
RenderCacheDuration: PointerOf(time.Hour * 12),
EnableLocks: true,
},
}
)
@@ -125,6 +126,7 @@ type RedisConfig struct {
type CacheConfig struct {
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
RenderCacheDuration *time.Duration `yaml:"render_cache_duration"`
EnableLocks bool `yaml:"enable_locks"`
}
// ReadFile reads the configuration from the file and parses it as YAML.

View File

@@ -8,30 +8,43 @@ import (
"image/draw"
"time"
"github.com/go-redis/redis/v8"
"github.com/go-redsync/redsync/v4"
redsyncredis "github.com/go-redsync/redsync/v4/redis"
redsyncredislib "github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/redis/go-redis/v9"
)
const defaultTimeout = 5 * time.Second
// Redis is a utility client for reading and writing values to the Redis server.
type Redis struct {
conn *redis.Client
Client *redis.Client
Pool *redsyncredis.Pool
SyncClient *redsync.Redsync
}
// Connect connects to the Redis server using the configuration values provided.
func (r *Redis) Connect(conf RedisConfig) error {
c := redis.NewClient(&redis.Options{
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
r.Client = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", conf.Host, conf.Port),
Username: conf.User,
Password: conf.Password,
DB: conf.Database,
})
r.conn = c
if err := r.Client.Ping(ctx).Err(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
pool := redsyncredislib.NewPool(r.Client)
defer cancel()
r.SyncClient = redsync.New(pool)
return c.Ping(ctx).Err()
return nil
}
// Scan returns all keys by the pattern in the Redis database.
@@ -40,7 +53,7 @@ func (r *Redis) Scan(cursor uint64, pattern string, count int64) ([]string, uint
defer cancel()
res := r.conn.Scan(ctx, cursor, pattern, count)
res := r.Client.Scan(ctx, cursor, pattern, count)
if err := res.Err(); err != nil {
return nil, 0, err
@@ -55,7 +68,7 @@ func (r *Redis) GetString(key string) (string, bool, error) {
defer cancel()
existsResult := r.conn.Exists(ctx, key)
existsResult := r.Client.Exists(ctx, key)
if err := existsResult.Err(); err != nil {
return "", false, err
@@ -65,7 +78,7 @@ func (r *Redis) GetString(key string) (string, bool, error) {
return "", false, nil
}
result := r.conn.Get(ctx, key)
result := r.Client.Get(ctx, key)
return result.Val(), true, result.Err()
}
@@ -76,7 +89,7 @@ func (r *Redis) GetBytes(key string) ([]byte, error) {
defer cancel()
existsResult := r.conn.Exists(ctx, key)
existsResult := r.Client.Exists(ctx, key)
if err := existsResult.Err(); err != nil {
return nil, err
@@ -86,7 +99,7 @@ func (r *Redis) GetBytes(key string) ([]byte, error) {
return nil, nil
}
result := r.conn.Get(ctx, key)
result := r.Client.Get(ctx, key)
if err := result.Err(); err != nil {
return nil, err
@@ -132,7 +145,7 @@ func (r *Redis) Exists(key string) (bool, error) {
defer cancel()
result := r.conn.Exists(ctx, key)
result := r.Client.Exists(ctx, key)
return result.Val() == 1, result.Err()
}
@@ -143,7 +156,7 @@ func (r *Redis) Delete(key string) error {
defer cancel()
return r.conn.Del(ctx, key).Err()
return r.Client.Del(ctx, key).Err()
}
// Set puts the key-value into the database with an optional TTL value.
@@ -152,10 +165,52 @@ func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error {
defer cancel()
return r.conn.Set(ctx, key, value, ttl).Err()
return r.Client.Set(ctx, key, value, ttl).Err()
}
// NewMutex creates a new mutually exclusive lock that only one process can hold.
func (r *Redis) NewMutex(name string) *Mutex {
if r.Client == nil || r.SyncClient == nil {
return &Mutex{
Mutex: nil,
}
}
return &Mutex{
Mutex: r.SyncClient.NewMutex(name),
}
}
// Close closes the connection to the database.
func (r *Redis) Close() error {
return r.conn.Close()
return r.Client.Close()
}
// Mutex is a mutually exclusive lock held across all processes.
type Mutex struct {
Mutex *redsync.Mutex
}
// Lock will lock the mutex so no other process can hold it.
func (m *Mutex) Lock() error {
if m.Mutex == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return m.Mutex.LockContext(ctx)
}
// Unlock will allow any other process to obtain a lock with the same key.
func (m *Mutex) Unlock() error {
if m.Mutex == nil {
return nil
}
_, err := m.Mutex.Unlock()
return err
}

View File

@@ -35,7 +35,6 @@ type QueryParams struct {
Scale int `query:"scale"`
Download bool `query:"download"`
Overlay bool `query:"overlay"`
Fallback bool `query:"fallback"`
}
// PointerOf returns the value of the first argument as a pointer.
@@ -45,6 +44,13 @@ func PointerOf[T any](v T) *T {
// Render will render the image using the specified details and return the result.
func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) {
if conf.Cache.EnableLocks {
mutex := r.NewMutex(fmt.Sprintf("render-lock:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid))
mutex.Lock()
defer mutex.Unlock()
}
cache, err := GetCachedRenderResult(renderType, uuid, opts)
if err != nil {
@@ -195,6 +201,13 @@ func FetchImage(url string) (*image.NRGBA, error) {
// GetPlayerSkin fetches the skin of the Minecraft player by the UUID.
func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
if conf.Cache.EnableLocks {
mutex := r.NewMutex(fmt.Sprintf("skin-lock:%s", uuid))
mutex.Lock()
defer mutex.Unlock()
}
if conf.Cache.SkinCacheDuration != nil {
cache, ok, err := r.GetNRGBA(fmt.Sprintf("skin:%s", uuid))