mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-04 04:23:01 +00:00
252 lines
6.0 KiB
Go
252 lines
6.0 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
var (
|
|
ErrCacheMiss = errors.New("cache miss")
|
|
ErrCacheSet = errors.New("cache set failed")
|
|
)
|
|
|
|
// Service provides high-level caching operations
|
|
type Service struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
// NewService creates a new cache service
|
|
func NewService(client *redis.Client) *Service {
|
|
return &Service{client: client}
|
|
}
|
|
|
|
// Get retrieves a value from cache and unmarshals it into the target
|
|
func (s *Service) Get(ctx context.Context, key string, target interface{}) error {
|
|
data, err := s.client.Get(ctx, key).Bytes()
|
|
if err != nil {
|
|
if errors.Is(err, redis.Nil) {
|
|
return ErrCacheMiss
|
|
}
|
|
return fmt.Errorf("cache get: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(data, target); err != nil {
|
|
return fmt.Errorf("cache unmarshal: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set stores a value in cache with the given TTL
|
|
func (s *Service) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("cache marshal: %w", err)
|
|
}
|
|
|
|
if err := s.client.Set(ctx, key, data, ttl).Err(); err != nil {
|
|
return fmt.Errorf("%w: %v", ErrCacheSet, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete removes a key from cache
|
|
func (s *Service) Delete(ctx context.Context, keys ...string) error {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := s.client.Del(ctx, keys...).Err(); err != nil {
|
|
return fmt.Errorf("cache delete: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Exists checks if a key exists in cache
|
|
func (s *Service) Exists(ctx context.Context, key string) (bool, error) {
|
|
count, err := s.client.Exists(ctx, key).Result()
|
|
if err != nil {
|
|
return false, fmt.Errorf("cache exists: %w", err)
|
|
}
|
|
|
|
return count > 0, nil
|
|
}
|
|
|
|
// Expire sets a TTL on an existing key
|
|
func (s *Service) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
|
if err := s.client.Expire(ctx, key, ttl).Err(); err != nil {
|
|
return fmt.Errorf("cache expire: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TTL returns the remaining time to live for a key
|
|
func (s *Service) TTL(ctx context.Context, key string) (time.Duration, error) {
|
|
ttl, err := s.client.TTL(ctx, key).Result()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cache ttl: %w", err)
|
|
}
|
|
|
|
return ttl, nil
|
|
}
|
|
|
|
// Increment atomically increments a counter
|
|
func (s *Service) Increment(ctx context.Context, key string) (int64, error) {
|
|
val, err := s.client.Incr(ctx, key).Result()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cache increment: %w", err)
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
// IncrementBy atomically increments a counter by a specific amount
|
|
func (s *Service) IncrementBy(ctx context.Context, key string, amount int64) (int64, error) {
|
|
val, err := s.client.IncrBy(ctx, key, amount).Result()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cache increment by: %w", err)
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
// SetNX sets a key only if it doesn't exist (useful for locks)
|
|
func (s *Service) SetNX(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return false, fmt.Errorf("cache marshal: %w", err)
|
|
}
|
|
|
|
ok, err := s.client.SetNX(ctx, key, data, ttl).Result()
|
|
if err != nil {
|
|
return false, fmt.Errorf("cache setnx: %w", err)
|
|
}
|
|
|
|
return ok, nil
|
|
}
|
|
|
|
// GetSet atomically sets a new value and returns the old value
|
|
func (s *Service) GetSet(ctx context.Context, key string, newValue interface{}, target interface{}) error {
|
|
data, err := json.Marshal(newValue)
|
|
if err != nil {
|
|
return fmt.Errorf("cache marshal: %w", err)
|
|
}
|
|
|
|
oldData, err := s.client.GetSet(ctx, key, data).Bytes()
|
|
if err != nil {
|
|
if errors.Is(err, redis.Nil) {
|
|
return ErrCacheMiss
|
|
}
|
|
return fmt.Errorf("cache getset: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(oldData, target); err != nil {
|
|
return fmt.Errorf("cache unmarshal: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MGet retrieves multiple keys at once
|
|
func (s *Service) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
|
|
if len(keys) == 0 {
|
|
return []interface{}{}, nil
|
|
}
|
|
|
|
values, err := s.client.MGet(ctx, keys...).Result()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cache mget: %w", err)
|
|
}
|
|
|
|
return values, nil
|
|
}
|
|
|
|
// MSet sets multiple key-value pairs at once
|
|
func (s *Service) MSet(ctx context.Context, pairs map[string]interface{}) error {
|
|
if len(pairs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Convert map to slice of interface{} for Redis
|
|
args := make([]interface{}, 0, len(pairs)*2)
|
|
for key, value := range pairs {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("cache marshal %s: %w", key, err)
|
|
}
|
|
args = append(args, key, data)
|
|
}
|
|
|
|
if err := s.client.MSet(ctx, args...).Err(); err != nil {
|
|
return fmt.Errorf("cache mset: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FlushDB clears all keys in the current database (use with caution!)
|
|
func (s *Service) FlushDB(ctx context.Context) error {
|
|
if err := s.client.FlushDB(ctx).Err(); err != nil {
|
|
return fmt.Errorf("cache flush: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Keys returns all keys matching a pattern
|
|
func (s *Service) Keys(ctx context.Context, pattern string) ([]string, error) {
|
|
keys, err := s.client.Keys(ctx, pattern).Result()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cache keys: %w", err)
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// Scan iterates over keys matching a pattern (better than Keys for large datasets)
|
|
func (s *Service) Scan(ctx context.Context, pattern string, count int64) ([]string, error) {
|
|
var keys []string
|
|
var cursor uint64
|
|
|
|
for {
|
|
var batch []string
|
|
var err error
|
|
|
|
batch, cursor, err = s.client.Scan(ctx, cursor, pattern, count).Result()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cache scan: %w", err)
|
|
}
|
|
|
|
keys = append(keys, batch...)
|
|
|
|
if cursor == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// Ping checks if the cache is responsive
|
|
func (s *Service) Ping(ctx context.Context) error {
|
|
return s.client.Ping(ctx).Err()
|
|
}
|
|
|
|
// Close closes the cache connection
|
|
func (s *Service) Close() error {
|
|
return s.client.Close()
|
|
}
|
|
|
|
// Client returns the underlying Redis client for advanced operations
|
|
func (s *Service) Client() *redis.Client {
|
|
return s.client
|
|
}
|