| package redis
|
|
|
| import (
|
| "context"
|
| "encoding/json"
|
| "errors"
|
| "fmt"
|
| "time"
|
|
|
| lib_store "github.com/eko/gocache/lib/v4/store"
|
| redis "github.com/redis/go-redis/v9"
|
| )
|
|
|
|
|
| type RedisClientInterface interface {
|
| Get(ctx context.Context, key string) *redis.StringCmd
|
| TTL(ctx context.Context, key string) *redis.DurationCmd
|
| Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd
|
| Set(ctx context.Context, key string, values any, expiration time.Duration) *redis.StatusCmd
|
| Del(ctx context.Context, keys ...string) *redis.IntCmd
|
| FlushAll(ctx context.Context) *redis.StatusCmd
|
| SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd
|
| SMembers(ctx context.Context, key string) *redis.StringSliceCmd
|
| }
|
|
|
| const (
|
|
|
| RedisType = "redis"
|
|
|
| RedisTagPattern = "gocache_tag_%s"
|
| )
|
|
|
|
|
| type RedisStore[T any] struct {
|
| client RedisClientInterface
|
| options *lib_store.Options
|
| }
|
|
|
|
|
| func NewRedisStore[T any](client RedisClientInterface, options ...lib_store.Option) *RedisStore[T] {
|
| return &RedisStore[T]{
|
| client: client,
|
| options: lib_store.ApplyOptions(options...),
|
| }
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) Get(ctx context.Context, key any) (any, error) {
|
| var result T
|
|
|
|
|
| object, err := gs.client.Get(ctx, key.(string)).Result()
|
| if errors.Is(err, redis.Nil) {
|
| return result, lib_store.NotFoundWithCause(err)
|
| }
|
|
|
| if err != nil {
|
| return result, err
|
| }
|
|
|
|
|
| if err := json.Unmarshal([]byte(object), &result); err != nil {
|
| var zero T
|
|
|
| return zero, err
|
| }
|
|
|
| return result, nil
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) GetWithTTL(ctx context.Context, key any) (any, time.Duration, error) {
|
| var result T
|
|
|
| keyString, ok := key.(string)
|
| if !ok {
|
| return result, 0, lib_store.NotFoundWithCause(fmt.Errorf("expected string key, got %T", key))
|
| }
|
|
|
| object, err := gs.client.Get(ctx, keyString).Result()
|
| if errors.Is(err, redis.Nil) {
|
| return result, 0, lib_store.NotFoundWithCause(err)
|
| }
|
|
|
| if err != nil {
|
| return result, 0, err
|
| }
|
|
|
|
|
| if err := json.Unmarshal([]byte(object), &result); err != nil {
|
| var zero T
|
|
|
| return zero, 0, err
|
| }
|
|
|
| ttl, err := gs.client.TTL(ctx, keyString).Result()
|
| if err != nil {
|
| var zero T
|
| return zero, 0, err
|
| }
|
|
|
| return result, ttl, err
|
| }
|
|
|
|
|
| func (s *RedisStore[T]) Set(ctx context.Context, key any, value any, options ...lib_store.Option) error {
|
| opts := lib_store.ApplyOptionsWithDefault(s.options, options...)
|
|
|
| raw, err := json.Marshal(value)
|
| if err != nil {
|
| return err
|
| }
|
|
|
| encodedValue := string(raw)
|
|
|
|
|
| err = s.client.Set(ctx, key.(string), encodedValue, opts.Expiration).Err()
|
| if err != nil {
|
| return err
|
| }
|
|
|
| if tags := opts.Tags; len(tags) > 0 {
|
| if ttl := opts.TagsTTL; ttl == 0 {
|
| s.setTags(ctx, key, tags)
|
| } else {
|
| s.setTagsWithTTL(ctx, key, tags, ttl)
|
| }
|
| }
|
|
|
| return nil
|
| }
|
|
|
| func (s *RedisStore[T]) setTagsWithTTL(ctx context.Context, key any, tags []string, ttl time.Duration) {
|
| for _, tag := range tags {
|
| tagKey := fmt.Sprintf(RedisTagPattern, tag)
|
|
|
| s.client.SAdd(ctx, tagKey, key.(string))
|
| s.client.Expire(ctx, tagKey, ttl)
|
| }
|
| }
|
|
|
| func (s *RedisStore[T]) setTags(ctx context.Context, key any, tags []string) {
|
| s.setTagsWithTTL(ctx, key, tags, 720*time.Hour)
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) Delete(ctx context.Context, key any) error {
|
|
|
| return gs.client.Del(ctx, key.(string)).Err()
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) GetType() string {
|
| return RedisType
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) Clear(ctx context.Context) error {
|
| return gs.client.FlushAll(ctx).Err()
|
| }
|
|
|
|
|
| func (gs *RedisStore[T]) Invalidate(ctx context.Context, options ...lib_store.InvalidateOption) error {
|
| return gs.client.FlushAll(ctx).Err()
|
| }
|
|
|