veza/veza-backend-api/internal/services/cache_service.go
2025-12-12 21:34:34 -05:00

339 lines
10 KiB
Go

//! Service de cache Redis pour optimiser les performances
//!
//! Ce service implémente une stratégie cache-aside avec invalidation automatique
//! pour améliorer les performances des requêtes fréquentes.
package services
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// CacheService gère le cache Redis avec différentes stratégies
type CacheService struct {
client *redis.Client
logger *zap.Logger
}
// CacheConfig contient la configuration du cache
type CacheConfig struct {
DefaultTTL time.Duration
UserTTL time.Duration
TrackTTL time.Duration
RoomTTL time.Duration
}
// DefaultCacheConfig retourne la configuration par défaut du cache
func DefaultCacheConfig() *CacheConfig {
return &CacheConfig{
DefaultTTL: 5 * time.Minute,
UserTTL: 5 * time.Minute,
TrackTTL: 30 * time.Minute,
RoomTTL: 1 * time.Minute,
}
}
// NewCacheService crée un nouveau service de cache
func NewCacheService(client *redis.Client, logger *zap.Logger) *CacheService {
return &CacheService{
client: client,
logger: logger,
}
}
// Set stocke une valeur dans le cache avec TTL
func (c *CacheService) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("failed to marshal value: %w", err)
}
err = c.client.Set(ctx, key, data, ttl).Err()
if err != nil {
c.logger.Error("Failed to set cache value",
zap.String("key", key),
zap.Error(err))
return err
}
c.logger.Debug("Cache value set",
zap.String("key", key),
zap.Duration("ttl", ttl))
return nil
}
// Get récupère une valeur du cache
func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return ErrCacheMiss
}
c.logger.Error("Failed to get cache value",
zap.String("key", key),
zap.Error(err))
return err
}
err = json.Unmarshal([]byte(data), dest)
if err != nil {
c.logger.Error("Failed to unmarshal cache value",
zap.String("key", key),
zap.Error(err))
return err
}
c.logger.Debug("Cache value retrieved", zap.String("key", key))
return nil
}
// Delete supprime une valeur du cache
func (c *CacheService) Delete(ctx context.Context, key string) error {
err := c.client.Del(ctx, key).Err()
if err != nil {
c.logger.Error("Failed to delete cache value",
zap.String("key", key),
zap.Error(err))
return err
}
c.logger.Debug("Cache value deleted", zap.String("key", key))
return nil
}
// DeletePattern supprime toutes les clés correspondant à un pattern
func (c *CacheService) DeletePattern(ctx context.Context, pattern string) error {
keys, err := c.client.Keys(ctx, pattern).Result()
if err != nil {
c.logger.Error("Failed to get keys by pattern",
zap.String("pattern", pattern),
zap.Error(err))
return err
}
if len(keys) > 0 {
err = c.client.Del(ctx, keys...).Err()
if err != nil {
c.logger.Error("Failed to delete keys by pattern",
zap.String("pattern", pattern),
zap.Error(err))
return err
}
c.logger.Debug("Cache keys deleted by pattern",
zap.String("pattern", pattern),
zap.Int("count", len(keys)))
}
return nil
}
// Exists vérifie si une clé existe dans le cache
func (c *CacheService) Exists(ctx context.Context, key string) (bool, error) {
count, err := c.client.Exists(ctx, key).Result()
if err != nil {
c.logger.Error("Failed to check cache key existence",
zap.String("key", key),
zap.Error(err))
return false, err
}
return count > 0, nil
}
// SetUser met en cache les données d'un utilisateur
func (c *CacheService) SetUser(ctx context.Context, userID uuid.UUID, user interface{}, config *CacheConfig) error {
key := fmt.Sprintf("user:%d", userID)
return c.Set(ctx, key, user, config.UserTTL)
}
// GetUser récupère les données d'un utilisateur depuis le cache
func (c *CacheService) GetUser(ctx context.Context, userID uuid.UUID, dest interface{}) error {
key := fmt.Sprintf("user:%d", userID)
return c.Get(ctx, key, dest)
}
// DeleteUser supprime les données d'un utilisateur du cache
func (c *CacheService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
key := fmt.Sprintf("user:%d", userID)
return c.Delete(ctx, key)
}
// SetTrack met en cache les métadonnées d'un track
func (c *CacheService) SetTrack(ctx context.Context, trackID int64, track interface{}, config *CacheConfig) error {
key := fmt.Sprintf("track:%d", trackID)
return c.Set(ctx, key, track, config.TrackTTL)
}
// GetTrack récupère les métadonnées d'un track depuis le cache
func (c *CacheService) GetTrack(ctx context.Context, trackID int64, dest interface{}) error {
key := fmt.Sprintf("track:%d", trackID)
return c.Get(ctx, key, dest)
}
// DeleteTrack supprime les métadonnées d'un track du cache
func (c *CacheService) DeleteTrack(ctx context.Context, trackID int64) error {
key := fmt.Sprintf("track:%d", trackID)
return c.Delete(ctx, key)
}
// SetRoom met en cache les données d'une room/conversation
func (c *CacheService) SetRoom(ctx context.Context, roomID int64, room interface{}, config *CacheConfig) error {
key := fmt.Sprintf("room:%d", roomID)
return c.Set(ctx, key, room, config.RoomTTL)
}
// GetRoom récupère les données d'une room depuis le cache
func (c *CacheService) GetRoom(ctx context.Context, roomID int64, dest interface{}) error {
key := fmt.Sprintf("room:%d", roomID)
return c.Get(ctx, key, dest)
}
// DeleteRoom supprime les données d'une room du cache
func (c *CacheService) DeleteRoom(ctx context.Context, roomID int64) error {
key := fmt.Sprintf("room:%d", roomID)
return c.Delete(ctx, key)
}
// SetMessages met en cache une liste de messages
func (c *CacheService) SetMessages(ctx context.Context, roomID int64, page int, messages interface{}, config *CacheConfig) error {
key := fmt.Sprintf("messages:%d:page:%d", roomID, page)
return c.Set(ctx, key, messages, config.RoomTTL)
}
// GetMessages récupère une liste de messages depuis le cache
func (c *CacheService) GetMessages(ctx context.Context, roomID int64, page int, dest interface{}) error {
key := fmt.Sprintf("messages:%d:page:%d", roomID, page)
return c.Get(ctx, key, dest)
}
// DeleteRoomMessages supprime tous les messages d'une room du cache
func (c *CacheService) DeleteRoomMessages(ctx context.Context, roomID int64) error {
pattern := fmt.Sprintf("messages:%d:*", roomID)
return c.DeletePattern(ctx, pattern)
}
// SetUserTracks met en cache la liste des tracks d'un utilisateur
func (c *CacheService) SetUserTracks(ctx context.Context, userID uuid.UUID, page int, tracks interface{}, config *CacheConfig) error {
key := fmt.Sprintf("user_tracks:%d:page:%d", userID, page)
return c.Set(ctx, key, tracks, config.TrackTTL)
}
// GetUserTracks récupère la liste des tracks d'un utilisateur depuis le cache
func (c *CacheService) GetUserTracks(ctx context.Context, userID uuid.UUID, page int, dest interface{}) error {
key := fmt.Sprintf("user_tracks:%d:page:%d", userID, page)
return c.Get(ctx, key, dest)
}
// DeleteUserTracks supprime tous les tracks d'un utilisateur du cache
func (c *CacheService) DeleteUserTracks(ctx context.Context, userID uuid.UUID) error {
pattern := fmt.Sprintf("user_tracks:%d:*", userID)
return c.DeletePattern(ctx, pattern)
}
// SetSearchResults met en cache les résultats de recherche
func (c *CacheService) SetSearchResults(ctx context.Context, query string, results interface{}, config *CacheConfig) error {
key := fmt.Sprintf("search:%s", query)
return c.Set(ctx, key, results, config.DefaultTTL)
}
// GetSearchResults récupère les résultats de recherche depuis le cache
func (c *CacheService) GetSearchResults(ctx context.Context, query string, dest interface{}) error {
key := fmt.Sprintf("search:%s", query)
return c.Get(ctx, key, dest)
}
// InvalidateUserCache invalide tout le cache lié à un utilisateur
func (c *CacheService) InvalidateUserCache(ctx context.Context, userID uuid.UUID) error {
patterns := []string{
fmt.Sprintf("user:%d", userID),
fmt.Sprintf("user_tracks:%d:*", userID),
fmt.Sprintf("user_sessions:%d:*", userID),
}
for _, pattern := range patterns {
if err := c.DeletePattern(ctx, pattern); err != nil {
c.logger.Error("Failed to invalidate user cache pattern",
zap.String("pattern", pattern),
zap.Error(err))
}
}
c.logger.Info("User cache invalidated", zap.String("user_id", userID.String()))
return nil
}
// InvalidateTrackCache invalide tout le cache lié à un track
func (c *CacheService) InvalidateTrackCache(ctx context.Context, trackID int64) error {
patterns := []string{
fmt.Sprintf("track:%d", trackID),
"search:*", // Invalider les recherches car le track peut apparaître dans les résultats
}
for _, pattern := range patterns {
if err := c.DeletePattern(ctx, pattern); err != nil {
c.logger.Error("Failed to invalidate track cache pattern",
zap.String("pattern", pattern),
zap.Error(err))
}
}
c.logger.Info("Track cache invalidated", zap.Int64("track_id", trackID))
return nil
}
// InvalidateRoomCache invalide tout le cache lié à une room
func (c *CacheService) InvalidateRoomCache(ctx context.Context, roomID int64) error {
patterns := []string{
fmt.Sprintf("room:%d", roomID),
fmt.Sprintf("messages:%d:*", roomID),
}
for _, pattern := range patterns {
if err := c.DeletePattern(ctx, pattern); err != nil {
c.logger.Error("Failed to invalidate room cache pattern",
zap.String("pattern", pattern),
zap.Error(err))
}
}
c.logger.Info("Room cache invalidated", zap.Int64("room_id", roomID))
return nil
}
// GetStats retourne les statistiques du cache
func (c *CacheService) GetStats(ctx context.Context) (*CacheStats, error) {
info, err := c.client.Info(ctx, "memory", "stats").Result()
if err != nil {
return nil, err
}
// Parser les informations Redis pour extraire les métriques
stats := &CacheStats{
Info: info,
}
return stats, nil
}
// CacheStats contient les statistiques du cache
type CacheStats struct {
Info string `json:"info"`
}
// ErrCacheMiss est retourné quand une clé n'existe pas dans le cache
var ErrCacheMiss = fmt.Errorf("cache miss")
// Close ferme la connexion Redis
func (c *CacheService) Close() error {
return c.client.Close()
}