package service import ( "context" "encoding/json" "fmt" "strings" "time" "bugulma/backend/internal/matching/engine" "github.com/go-redis/redis/v8" ) // CacheService provides caching layer for match results // Based on concept spec: Fast Matching (<100ms) using Redis cache type CacheService interface { GetMatches(ctx context.Context, key string) ([]*engine.Candidate, error) SetMatches(ctx context.Context, key string, matches []*engine.Candidate, ttl time.Duration) error InvalidateByResourceID(ctx context.Context, resourceID string) error InvalidateAll(ctx context.Context) error } // MemoryCacheService is an in-memory implementation for development // In production, use Redis implementation type MemoryCacheService struct { cache map[string]cachedMatches } type cachedMatches struct { matches []*engine.Candidate expiresAt time.Time } // NewMemoryCacheService creates a new in-memory cache service func NewMemoryCacheService() *MemoryCacheService { return &MemoryCacheService{ cache: make(map[string]cachedMatches), } } // GetMatches retrieves cached matches func (c *MemoryCacheService) GetMatches(ctx context.Context, key string) ([]*engine.Candidate, error) { cached, exists := c.cache[key] if !exists { return nil, fmt.Errorf("cache miss") } if time.Now().After(cached.expiresAt) { delete(c.cache, key) return nil, fmt.Errorf("cache expired") } return cached.matches, nil } // SetMatches caches match results func (c *MemoryCacheService) SetMatches(ctx context.Context, key string, matches []*engine.Candidate, ttl time.Duration) error { c.cache[key] = cachedMatches{ matches: matches, expiresAt: time.Now().Add(ttl), } return nil } // InvalidateByResourceID invalidates cache entries containing a specific resource func (c *MemoryCacheService) InvalidateByResourceID(ctx context.Context, resourceID string) error { // Simple implementation: clear entire cache // In production, maintain resource-to-cache-key mappings c.cache = make(map[string]cachedMatches) return nil } // InvalidateAll clears entire cache func (c *MemoryCacheService) InvalidateAll(ctx context.Context) error { c.cache = make(map[string]cachedMatches) return nil } // GenerateCacheKey generates a cache key for match queries func GenerateCacheKey(resourceType string, lat, lng, maxDistanceKm float64) string { return fmt.Sprintf("matches:%s:%.4f:%.4f:%.1f", resourceType, lat, lng, maxDistanceKm) } // RedisCacheService is a Redis-backed cache implementation for production use type RedisCacheService struct { client *redis.Client } // NewRedisCacheService creates a new Redis cache service func NewRedisCacheService(redisURL string) (*RedisCacheService, error) { opt, err := redis.ParseURL(redisURL) if err != nil { return nil, fmt.Errorf("failed to parse Redis URL: %w", err) } client := redis.NewClient(opt) // Test connection ctx := context.Background() if err := client.Ping(ctx).Err(); err != nil { return nil, fmt.Errorf("failed to connect to Redis: %w", err) } return &RedisCacheService{ client: client, }, nil } // GetMatches retrieves cached matches from Redis func (r *RedisCacheService) GetMatches(ctx context.Context, key string) ([]*engine.Candidate, error) { val, err := r.client.Get(ctx, key).Result() if err == redis.Nil { return nil, fmt.Errorf("cache miss") } if err != nil { return nil, fmt.Errorf("failed to get cache key %s: %w", key, err) } var matches []*engine.Candidate if err := json.Unmarshal([]byte(val), &matches); err != nil { return nil, fmt.Errorf("failed to unmarshal cached data: %w", err) } return matches, nil } // SetMatches caches match results in Redis with TTL func (r *RedisCacheService) SetMatches(ctx context.Context, key string, matches []*engine.Candidate, ttl time.Duration) error { data, err := json.Marshal(matches) if err != nil { return fmt.Errorf("failed to marshal matches: %w", err) } if err := r.client.Set(ctx, key, data, ttl).Err(); err != nil { return fmt.Errorf("failed to set cache key %s: %w", key, err) } return nil } // InvalidateByResourceID invalidates cache entries containing a specific resource // Uses Redis SCAN to efficiently find and delete matching keys func (r *RedisCacheService) InvalidateByResourceID(ctx context.Context, resourceID string) error { // Use SCAN to find all cache entries containing this resource ID // Pattern: matches:resourceType:resourceID:lat:lng:distance pattern := fmt.Sprintf("*%s*", resourceID) var cursor uint64 var keys []string var err error // SCAN with pattern to find matching keys for { keys, cursor, err = r.client.Scan(ctx, cursor, pattern, 100).Result() if err != nil { return fmt.Errorf("failed to scan cache keys: %w", err) } // Filter keys that actually contain the resource ID in match context var keysToDelete []string for _, key := range keys { if strings.Contains(key, resourceID) { keysToDelete = append(keysToDelete, key) } } // Delete found keys if len(keysToDelete) > 0 { if err := r.client.Del(ctx, keysToDelete...).Err(); err != nil { return fmt.Errorf("failed to delete cache keys: %w", err) } } if cursor == 0 { break } } return nil } // InvalidateAll clears entire cache database func (r *RedisCacheService) InvalidateAll(ctx context.Context) error { if err := r.client.FlushDB(ctx).Err(); err != nil { return fmt.Errorf("failed to flush Redis database: %w", err) } return nil } // Close closes the Redis connection func (r *RedisCacheService) Close() error { return r.client.Close() }