--- story_id: story_5_8 sprint_id: sprint_5 phase: Phase 2 - Resources status: not_started priority: P1 story_points: 5 assignee: backend estimated_days: 2 created_date: 2025-11-06 dependencies: [story_5_5] --- # Story 5.8: Redis Caching Integration **Phase**: Phase 2 - Resources (Week 3-4) **Priority**: P1 HIGH **Estimated Effort**: 5 Story Points (2 days) ## User Story **As a** System Architect **I want** Redis caching for frequently accessed MCP Resources **So that** AI agents get fast responses and database load is reduced ## Business Value Performance optimization delivers: - 30-50% faster response times (from 200ms to 80ms) - Reduced database load (80% cache hit rate target) - Better scalability (support more concurrent AI agents) - Improved user experience (faster AI responses) ## Acceptance Criteria ### AC1: Redis Integration - [ ] Redis client configured (StackExchange.Redis) - [ ] Connection pooling and retry logic - [ ] Health checks for Redis availability ### AC2: Cache-Aside Pattern - [ ] Check cache before database query - [ ] Populate cache on cache miss - [ ] Return cached data on cache hit ### AC3: TTL Configuration - [ ] `projects.list`: 5 minutes TTL - [ ] `users.list`: 5 minutes TTL - [ ] `sprints.current`: 2 minutes TTL - [ ] `issues.search`: 2 minutes TTL (cache by query params) ### AC4: Cache Invalidation - [ ] Invalidate on data updates (domain events) - [ ] Manual cache clear API endpoint - [ ] Tenant-scoped cache keys ### AC5: Performance Metrics - [ ] Track cache hit rate (target > 80%) - [ ] Track cache miss rate - [ ] Track response time improvement - [ ] Export metrics for monitoring ### AC6: Testing - [ ] Unit tests for caching logic - [ ] Integration tests (cache hit/miss scenarios) - [ ] Performance benchmarks (with/without cache) ## Technical Design ### Cache Service Interface ```csharp public interface IMcpCacheService { Task GetAsync(string key, CancellationToken cancellationToken = default); Task SetAsync(string key, T value, TimeSpan ttl, CancellationToken cancellationToken = default); Task RemoveAsync(string key, CancellationToken cancellationToken = default); Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default); } public class RedisMcpCacheService : IMcpCacheService { private readonly IConnectionMultiplexer _redis; private readonly ILogger _logger; public async Task GetAsync(string key, CancellationToken ct) { var db = _redis.GetDatabase(); var value = await db.StringGetAsync(key); if (!value.HasValue) { _logger.LogDebug("Cache miss: {Key}", key); return default; } _logger.LogDebug("Cache hit: {Key}", key); return JsonSerializer.Deserialize(value!); } public async Task SetAsync(string key, T value, TimeSpan ttl, CancellationToken ct) { var db = _redis.GetDatabase(); var json = JsonSerializer.Serialize(value); await db.StringSetAsync(key, json, ttl); _logger.LogDebug("Cache set: {Key} (TTL: {TTL}s)", key, ttl.TotalSeconds); } public async Task RemoveAsync(string key, CancellationToken ct) { var db = _redis.GetDatabase(); await db.KeyDeleteAsync(key); _logger.LogDebug("Cache removed: {Key}", key); } public async Task RemoveByPatternAsync(string pattern, CancellationToken ct) { var server = _redis.GetServer(_redis.GetEndPoints().First()); var keys = server.Keys(pattern: pattern); var db = _redis.GetDatabase(); foreach (var key in keys) { await db.KeyDeleteAsync(key); } _logger.LogDebug("Cache removed by pattern: {Pattern}", pattern); } } ``` ### Cached Resource Example ```csharp public class ProjectsListResource : IMcpResource { private readonly IProjectRepository _projectRepo; private readonly ITenantContext _tenantContext; private readonly IMcpCacheService _cache; public async Task GetContentAsync( McpResourceRequest request, CancellationToken cancellationToken) { var tenantId = _tenantContext.CurrentTenantId; var cacheKey = $"mcp:{tenantId}:projects.list"; // Try cache first var cached = await _cache.GetAsync(cacheKey, cancellationToken); if (cached != null) { return CreateResponse(cached); } // Cache miss - query database var projects = await _projectRepo.GetAllAsync(tenantId, cancellationToken); var projectDtos = projects.Select(MapToDto).ToArray(); // Populate cache await _cache.SetAsync(cacheKey, projectDtos, TimeSpan.FromMinutes(5), cancellationToken); return CreateResponse(projectDtos); } } ``` ### Cache Key Format ``` mcp:{tenantId}:{resourceUri}[:{params_hash}] Examples: - mcp:00000000-0000-0000-0000-000000000001:projects.list - mcp:00000000-0000-0000-0000-000000000001:issues.search:abc123 - mcp:00000000-0000-0000-0000-000000000001:sprints.current ``` ### Cache Invalidation (Domain Events) ```csharp public class ProjectUpdatedEventHandler : INotificationHandler { private readonly IMcpCacheService _cache; public async Task Handle(ProjectUpdatedEvent e, CancellationToken ct) { // Invalidate projects.list for this tenant await _cache.RemoveAsync($"mcp:{e.TenantId}:projects.list", ct); // Invalidate specific project await _cache.RemoveAsync($"mcp:{e.TenantId}:projects.get/{e.ProjectId}", ct); } } ``` ## Tasks ### Task 1: Redis Client Setup (2 hours) - [ ] Add StackExchange.Redis NuGet package - [ ] Configure connection string in `appsettings.json` - [ ] Create `RedisMcpCacheService` implementation - [ ] Add health checks **Files to Create**: - `ColaFlow.Modules.Mcp/Services/RedisMcpCacheService.cs` ### Task 2: Cache Service Interface (2 hours) - [ ] Create `IMcpCacheService` interface - [ ] Implement Get/Set/Remove methods - [ ] Add logging and metrics **Files to Create**: - `ColaFlow.Modules.Mcp/Contracts/IMcpCacheService.cs` ### Task 3: Update Resources with Caching (6 hours) - [ ] Update `ProjectsListResource` to use cache - [ ] Update `UsersListResource` to use cache - [ ] Update `SprintsCurrentResource` to use cache - [ ] Update `IssuesSearchResource` to use cache (with query hash) ### Task 4: Cache Invalidation Event Handlers (3 hours) - [ ] ProjectCreated/Updated/Deleted → invalidate projects cache - [ ] EpicCreated/Updated/Deleted → invalidate issues cache - [ ] StoryCreated/Updated/Deleted → invalidate issues cache - [ ] TaskCreated/Updated/Deleted → invalidate issues cache **Files to Create**: - `ColaFlow.Modules.Mcp/EventHandlers/CacheInvalidationEventHandlers.cs` ### Task 5: Performance Metrics (2 hours) - [ ] Track cache hit/miss rates - [ ] Track response time improvement - [ ] Log metrics to structured logs ### Task 6: Unit & Integration Tests (4 hours) - [ ] Test cache hit scenario - [ ] Test cache miss scenario - [ ] Test cache invalidation - [ ] Performance benchmarks (with/without cache) **Files to Create**: - `ColaFlow.Modules.Mcp.Tests/Services/RedisMcpCacheServiceTests.cs` - `ColaFlow.Modules.Mcp.Tests/Integration/CachingIntegrationTests.cs` ## Testing Strategy ### Performance Benchmarks ``` Scenario: projects.list (100 projects) - Without cache: 180ms (database query) - With cache: 60ms (67% improvement) - Cache hit rate: 85% ``` ### Integration Tests - Test cache hit after first request - Test cache miss on first request - Test cache invalidation on update - Test TTL expiration ## Dependencies **Prerequisites**: - Story 5.5 (Core Resources) - Resources to cache - Redis server running (Docker or cloud) **Optional**: Not blocking for M2 MVP ## Risks & Mitigation | Risk | Impact | Probability | Mitigation | |------|--------|-------------|------------| | Redis unavailable | Medium | Low | Fallback to database, circuit breaker | | Cache inconsistency | Medium | Medium | Short TTL, event-driven invalidation | | Memory usage | Low | Low | Set max memory limit in Redis | ## Definition of Done - [ ] Redis integration working - [ ] Cache hit rate > 80% - [ ] Response time improved by 30%+ - [ ] Cache invalidation working - [ ] All tests passing - [ ] Performance benchmarks documented ## Notes ### Why This Story Matters - **Performance**: 30-50% faster response times - **Scalability**: Reduce database load by 80% - **Cost**: Lower database resource usage - **User Experience**: Faster AI responses ### Redis Configuration ```json { "Redis": { "ConnectionString": "localhost:6379", "InstanceName": "colaflow:", "DefaultTTL": 300 } } ```