package cache import ( "context" "fmt" "time" "github.com/redis/go-redis/v9" "github.com/tdvorak/seen/backend/internal/config" "go.uber.org/zap" ) // Manager provides centralized cache management type Manager struct { client *redis.Client service *Service session *SessionCache catalog *CatalogCache download *DownloadCache log *zap.Logger } // NewManager creates a new cache manager with all cache services func NewManager(ctx context.Context, cfg config.CacheConfig, log *zap.Logger) (*Manager, error) { client, err := NewClient(ctx, cfg, log) if err != nil { return nil, fmt.Errorf("failed to create cache client: %w", err) } service := NewService(client) namespace := "seen" manager := &Manager{ client: client, service: service, session: NewSessionCache(service, namespace), catalog: NewCatalogCache(service, namespace), download: NewDownloadCache(service, namespace), log: log, } // Start background cleanup tasks go manager.startCleanupTasks(ctx) return manager, nil } // Client returns the underlying Redis client func (m *Manager) Client() *redis.Client { return m.client } // Service returns the cache service func (m *Manager) Service() *Service { return m.service } // Session returns the session cache func (m *Manager) Session() *SessionCache { return m.session } // Catalog returns the catalog cache func (m *Manager) Catalog() *CatalogCache { return m.catalog } // Download returns the download cache func (m *Manager) Download() *DownloadCache { return m.download } // Ping checks if the cache is responsive func (m *Manager) Ping(ctx context.Context) error { return m.service.Ping(ctx) } // Close closes all cache connections func (m *Manager) Close() error { m.log.Info("closing cache connections") return m.service.Close() } // Stats returns cache statistics func (m *Manager) Stats(ctx context.Context) (map[string]interface{}, error) { info, err := m.client.Info(ctx, "stats", "memory", "keyspace").Result() if err != nil { return nil, fmt.Errorf("failed to get cache stats: %w", err) } dbSize, err := m.client.DBSize(ctx).Result() if err != nil { return nil, fmt.Errorf("failed to get db size: %w", err) } stats := map[string]interface{}{ "dbSize": dbSize, "info": info, } return stats, nil } // FlushAll clears all cache data (use with extreme caution!) func (m *Manager) FlushAll(ctx context.Context) error { m.log.Warn("flushing all cache data") return m.service.FlushDB(ctx) } // startCleanupTasks starts background tasks for cache maintenance func (m *Manager) startCleanupTasks(ctx context.Context) { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): m.log.Info("stopping cache cleanup tasks") return case <-ticker.C: m.runCleanupTasks(ctx) } } } // runCleanupTasks performs periodic cache maintenance func (m *Manager) runCleanupTasks(ctx context.Context) { // Cleanup stale download progress (older than 1 hour) if err := m.download.CleanupStaleProgress(ctx, 1*time.Hour); err != nil { m.log.Error("failed to cleanup stale download progress", zap.Error(err)) } // Log cache stats stats, err := m.Stats(ctx) if err != nil { m.log.Error("failed to get cache stats", zap.Error(err)) return } if dbSize, ok := stats["dbSize"].(int64); ok { m.log.Debug("cache stats", zap.Int64("keys", dbSize)) } } // InvalidateUser removes all cached data for a user func (m *Manager) InvalidateUser(ctx context.Context, userID string) error { m.log.Info("invalidating user cache", zap.String("userId", userID)) // Invalidate user data if err := m.session.DeleteUser(ctx, userID); err != nil { m.log.Error("failed to delete user cache", zap.Error(err)) } // Invalidate user sessions if err := m.session.InvalidateUserSessions(ctx, userID); err != nil { m.log.Error("failed to invalidate user sessions", zap.Error(err)) } // Invalidate catalog data if err := m.catalog.InvalidateUserCatalog(ctx, userID); err != nil { m.log.Error("failed to invalidate user catalog", zap.Error(err)) } // Invalidate download data if err := m.download.InvalidateUserDownloads(ctx, userID); err != nil { m.log.Error("failed to invalidate user downloads", zap.Error(err)) } return nil } // WarmupCache pre-populates cache with common data func (m *Manager) WarmupCache(ctx context.Context) error { m.log.Info("warming up cache") // Add warmup logic here as needed // For example, pre-cache popular catalog sections return nil } // HealthCheck performs a comprehensive health check func (m *Manager) HealthCheck(ctx context.Context) error { // Check basic connectivity if err := m.Ping(ctx); err != nil { return fmt.Errorf("ping failed: %w", err) } // Test write operation testKey := "health:check" testValue := map[string]interface{}{ "timestamp": time.Now().UTC(), "test": true, } if err := m.service.Set(ctx, testKey, testValue, 10*time.Second); err != nil { return fmt.Errorf("write test failed: %w", err) } // Test read operation var readValue map[string]interface{} if err := m.service.Get(ctx, testKey, &readValue); err != nil { return fmt.Errorf("read test failed: %w", err) } // Cleanup test key if err := m.service.Delete(ctx, testKey); err != nil { m.log.Warn("failed to cleanup health check key", zap.Error(err)) } return nil } // GetKeysByPattern retrieves all keys matching a pattern func (m *Manager) GetKeysByPattern(ctx context.Context, pattern string) ([]string, error) { return m.service.Keys(ctx, pattern) } // DeleteKeysByPattern deletes all keys matching a pattern func (m *Manager) DeleteKeysByPattern(ctx context.Context, pattern string) error { keys, err := m.service.Keys(ctx, pattern) if err != nil { return err } if len(keys) == 0 { return nil } m.log.Info("deleting keys by pattern", zap.String("pattern", pattern), zap.Int("count", len(keys))) return m.service.Delete(ctx, keys...) }