asg-server/pkg/cache/redis.go
2025-09-24 13:47:55 +07:00

169 lines
3.5 KiB
Go

package cache
import (
"context"
"fmt"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
const (
redisCacheKey = "cache"
// getAndDeleteScript atomically gets and deletes a hash field
getAndDeleteScript = `
local value = redis.call('HGET', KEYS[1], ARGV[1])
if value then
redis.call('HDEL', KEYS[1], ARGV[1])
return value
else
return false
end
`
hgetallAndDeleteScript = `
local items = redis.call('HGETALL', KEYS[1])
if #items > 0 then
local ok = pcall(redis.call, 'UNLINK', KEYS[1])
if not ok then redis.call('DEL', KEYS[1]) end
end
return items
`
)
type redisCache struct {
client *redis.Client
key string
ttl time.Duration
}
func NewRedis(client *redis.Client, prefix string, ttl time.Duration) Cache {
if prefix != "" && !strings.HasSuffix(prefix, ":") {
prefix += ":"
}
return &redisCache{
client: client,
key: prefix + redisCacheKey,
ttl: ttl,
}
}
// Cleanup implements Cache.
func (r *redisCache) Cleanup(_ context.Context) error {
return nil
}
// Delete implements Cache.
func (r *redisCache) Delete(ctx context.Context, key string) error {
if err := r.client.HDel(ctx, r.key, key).Err(); err != nil {
return fmt.Errorf("can't delete cache item: %w", err)
}
return nil
}
// Drain implements Cache.
func (r *redisCache) Drain(ctx context.Context) (map[string]string, error) {
res, err := r.client.Eval(ctx, hgetallAndDeleteScript, []string{r.key}).Result()
if err != nil {
return nil, fmt.Errorf("can't drain cache: %w", err)
}
arr, ok := res.([]any)
if !ok || len(arr) == 0 {
return map[string]string{}, nil
}
out := make(map[string]string, len(arr)/2)
for i := 0; i < len(arr); i += 2 {
f, _ := arr[i].(string)
v, _ := arr[i+1].(string)
out[f] = v
}
return out, nil
}
// Get implements Cache.
func (r *redisCache) Get(ctx context.Context, key string) (string, error) {
val, err := r.client.HGet(ctx, r.key, key).Result()
if err != nil {
if err == redis.Nil {
return "", ErrKeyNotFound
}
return "", fmt.Errorf("can't get cache item: %w", err)
}
return val, nil
}
// GetAndDelete implements Cache.
func (r *redisCache) GetAndDelete(ctx context.Context, key string) (string, error) {
result, err := r.client.Eval(ctx, getAndDeleteScript, []string{r.key}, key).Result()
if err != nil {
return "", fmt.Errorf("can't get cache item: %w", err)
}
if value, ok := result.(string); ok {
return value, nil
}
return "", ErrKeyNotFound
}
// Set implements Cache.
func (r *redisCache) Set(ctx context.Context, key string, value string, opts ...Option) error {
options := new(options)
if r.ttl > 0 {
options.validUntil = time.Now().Add(r.ttl)
}
options.apply(opts...)
_, err := r.client.Pipelined(ctx, func(p redis.Pipeliner) error {
p.HSet(ctx, r.key, key, value)
if !options.validUntil.IsZero() {
p.HExpireAt(ctx, r.key, options.validUntil, key)
}
return nil
})
if err != nil {
return fmt.Errorf("can't set cache item: %w", err)
}
return nil
}
// SetOrFail implements Cache.
func (r *redisCache) SetOrFail(ctx context.Context, key string, value string, opts ...Option) error {
val, err := r.client.HSetNX(ctx, r.key, key, value).Result()
if err != nil {
return fmt.Errorf("can't set cache item: %w", err)
}
if !val {
return ErrKeyExists
}
options := new(options)
if r.ttl > 0 {
options.validUntil = time.Now().Add(r.ttl)
}
options.apply(opts...)
if !options.validUntil.IsZero() {
if err := r.client.HExpireAt(ctx, r.key, options.validUntil).Err(); err != nil {
return fmt.Errorf("can't set cache item ttl: %w", err)
}
}
return nil
}