Files
SEEN/backend/internal/integrations/cache/service.go
T
2026-04-10 12:06:24 +02:00

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
}