//! 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 PlaylistTTL 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, PlaylistTTL: 15 * 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 // BE-SVC-001: Implement caching layer for frequently accessed data func (c *CacheService) SetUser(ctx context.Context, userID uuid.UUID, user interface{}, config *CacheConfig) error { key := fmt.Sprintf("user:%s", userID.String()) 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:%s", userID.String()) 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:%s", userID.String()) return c.Delete(ctx, key) } // SetTrack met en cache les métadonnées d'un track // BE-SVC-001: Support UUID for tracks func (c *CacheService) SetTrack(ctx context.Context, trackID uuid.UUID, track interface{}, config *CacheConfig) error { key := fmt.Sprintf("track:%s", trackID.String()) 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 uuid.UUID, dest interface{}) error { key := fmt.Sprintf("track:%s", trackID.String()) 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 uuid.UUID) error { key := fmt.Sprintf("track:%s", trackID.String()) return c.Delete(ctx, key) } // SetPlaylist met en cache les données d'une playlist // BE-SVC-001: Add playlist caching func (c *CacheService) SetPlaylist(ctx context.Context, playlistID uuid.UUID, playlist interface{}, config *CacheConfig) error { key := fmt.Sprintf("playlist:%s", playlistID.String()) return c.Set(ctx, key, playlist, config.PlaylistTTL) } // GetPlaylist récupère les données d'une playlist depuis le cache func (c *CacheService) GetPlaylist(ctx context.Context, playlistID uuid.UUID, dest interface{}) error { key := fmt.Sprintf("playlist:%s", playlistID.String()) return c.Get(ctx, key, dest) } // DeletePlaylist supprime les données d'une playlist du cache func (c *CacheService) DeletePlaylist(ctx context.Context, playlistID uuid.UUID) error { key := fmt.Sprintf("playlist:%s", playlistID.String()) 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:%s:page:%d", userID.String(), 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:%s:page:%d", userID.String(), 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:%s:*", userID.String()) 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:%s", userID.String()), fmt.Sprintf("user_tracks:%s:*", userID.String()), fmt.Sprintf("user_sessions:%s:*", userID.String()), } 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 // BE-SVC-001: Support UUID for tracks func (c *CacheService) InvalidateTrackCache(ctx context.Context, trackID uuid.UUID) error { patterns := []string{ fmt.Sprintf("track:%s", trackID.String()), "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.String("track_id", trackID.String())) return nil } // InvalidatePlaylistCache invalide tout le cache lié à une playlist // BE-SVC-001: Add playlist cache invalidation func (c *CacheService) InvalidatePlaylistCache(ctx context.Context, playlistID uuid.UUID) error { patterns := []string{ fmt.Sprintf("playlist:%s", playlistID.String()), "search:*", // Invalider les recherches car la playlist 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 playlist cache pattern", zap.String("pattern", pattern), zap.Error(err)) } } c.logger.Info("Playlist cache invalidated", zap.String("playlist_id", playlistID.String())) 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() }