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

236 lines
5.9 KiB
Go

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...)
}