# Response Caching Optimization Strategy

## Overview

While N+1 batching fixes query execution patterns, response-level caching prevents queries from running at all. This document outlines caching strategies for common Jellyfin API endpoints to further reduce database load during page loads.

---

## 1. Caching Opportunities

### High-Impact Caching Targets (Easy Wins)

#### 1.1 Home Page Item Lists
These rarely change but are queried frequently:

```csharp
// Current Implementation (in Jellyfin.Api/Controllers)
public ActionResult<ItemsResult> GetItems(
    [FromQuery] string? includeItemTypes,
    [FromQuery] int limit = 16)
{
    // Execute query every time
    var items = _libraryManager.GetItems(filter);
    return Ok(items);
}

// Recommended: Add Response Caching
[ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any, VaryByHeader = "Authorization,Accept")]
public ActionResult<ItemsResult> GetItems(
    [FromQuery] string? includeItemTypes,
    [FromQuery] int limit = 16)
{
    var items = _libraryManager.GetItems(filter);
    return Ok(items);
}
```

**Cache Duration Recommendations:**
- Recently added: 30 seconds (updates frequently)
- Recommendations: 60 seconds
- Continue watching: 30 seconds (user-specific)
- Collections: 5 minutes
- Genres: 10 minutes (rarely changes)

#### 1.2 User-Specific Data
These depend on user and should be cached per-user:

```csharp
private readonly IDistributedCache _cache;

public async Task<ItemsResult> GetResumeItems(string userId)
{
    var cacheKey = $"resume_{userId}";
    var cached = await _cache.GetStringAsync(cacheKey);
    
    if (cached != null)
    {
        return JsonConvert.DeserializeObject<ItemsResult>(cached);
    }

    var items = _libraryManager.GetResumeItems(userId);
    
    // Cache for 30 seconds
    await _cache.SetStringAsync(cacheKey, JsonConvert.SerializeObject(items), 
        new DistributedCacheEntryOptions 
        { 
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30) 
        });
    
    return items;
}
```

### Medium-Impact Caching Targets (Moderate Complexity)

#### 1.3 Library Statistics
Change infrequently but used frequently:

```csharp
[ResponseCache(Duration = 300)]  // 5 minutes
public ActionResult<object> GetLibraryStats()
{
    // Expensive aggregation query
    var stats = new
    {
        MovieCount = libraryManager.GetItemCount(i => i is Movie),
        SeriesCount = libraryManager.GetItemCount(i => i is Series),
        EpisodeCount = libraryManager.GetItemCount(i => i is Episode),
    };
    return Ok(stats);
}
```

#### 1.4 Browse/Navigation Data
Metadata about libraries and collections:

```csharp
[ResponseCache(Duration = 600)]  // 10 minutes
public ActionResult<CollectionsResult> GetCollections()
{
    var collections = _libraryManager.GetCollections();
    return Ok(collections);
}
```

---

## 2. Implementation Strategies

### Strategy A: HTTP Response Caching (Simplest)

**Pros:**
- HTTP standard (understood by proxies, CDNs)
- Browser caches responses automatically
- Zero code changes needed (just add attribute)
- Works across multiple server instances

**Cons:**
- Cache key limited to URL + headers
- Can't cache POST requests easily
- Some clients ignore cache headers

**Implementation:**
```csharp
using Microsoft.AspNetCore.Mvc;

// In Startup.cs ConfigureServices()
services.AddResponseCaching();

// In Configure()
app.UseResponseCaching();

// On endpoints
[ResponseCache(
    Duration = 30,                                    // 30 seconds
    Location = ResponseCacheLocation.Any,            // HTTP layer
    VaryByHeader = "Authorization,Accept")]           // Vary by user/format
public ActionResult<ItemsResult> GetItems(...)
{
    // ...
}
```

**Current Status**: ⏳ Not yet implemented (recommended quick win)

### Strategy B: Distributed Cache (Scalable)

**Pros:**
- Works across multiple servers
- Survives server restarts
- Cache invalidation possible
- Can cache complex objects

**Cons:**
- Requires Redis or similar service
- Slightly more complex
- Network latency (though minimal)

**Implementation:**
```csharp
public class CachedLibraryController : ControllerBase
{
    private readonly IDistributedCache _cache;
    private readonly ILibraryManager _libraryManager;

    [HttpGet("Items")]
    public async Task<ActionResult<ItemsResult>> GetItems(
        [FromQuery] string userId,
        [FromQuery] string? includeItemTypes,
        [FromQuery] int limit = 16)
    {
        var cacheKey = $"items_{userId}_{includeItemTypes}_{limit}";
        
        var cached = await _cache.GetStringAsync(cacheKey);
        if (cached != null)
        {
            return Ok(JsonSerializer.Deserialize<ItemsResult>(cached));
        }

        var items = await _libraryManager.GetItemsAsync(
            new ItemFilter { IncludeItemTypes = includeItemTypes, Limit = limit });

        await _cache.SetStringAsync(cacheKey, 
            JsonSerializer.Serialize(items),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
            });

        return Ok(items);
    }
}
```

**Current Status**: ⏳ Not yet implemented (recommended for multi-server setups)

### Strategy C: In-Memory Caching (Current)

**Pros:**
- Fastest (in-process)
- Zero network latency
- Simple to implement

**Cons:**
- Single-server only
- Lost on restart
- Already using for ChildCount

**Implementation:**
```csharp
private readonly IMemoryCache _cache;

public ActionResult<ItemsResult> GetItems(...)
{
    var cacheKey = $"items_{cacheParams}";
    
    if (_cache.TryGetValue(cacheKey, out ItemsResult? cached))
    {
        return Ok(cached);
    }

    var items = _libraryManager.GetItems(...);

    _cache.Set(cacheKey, items, 
        new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30),
            SlidingExpiration = TimeSpan.FromSeconds(10)
        });

    return Ok(items);
}
```

**Current Status**: ✅ Using for ChildCount (could expand)

---

## 3. Cache Invalidation Strategy

### Automatic Invalidation Points

**When to Invalidate Cache:**

```csharp
public class CacheInvalidationService
{
    private readonly IDistributedCache _cache;
    private readonly ILogger _logger;

    // Called when items are added/modified/deleted
    public async Task OnItemChanged(BaseItem item)
    {
        var keysToInvalidate = new[]
        {
            // Invalidate all item lists that might include this item
            $"items_*",
            $"resume_*",
            $"recent_*",
            
            // Invalidate stats if item type changed
            $"stats_*",
            
            // Invalidate parent folder caches
            $"items_{item.ParentId}_*"
        };

        foreach (var pattern in keysToInvalidate)
        {
            // Note: Most IDistributedCache don't support wildcards
            // Implement pattern matching or use explicit keys
            _logger.LogInformation("Invalidating cache pattern: {Pattern}", pattern);
        }
    }

    // Called on library refresh
    public async Task OnLibraryRefresh()
    {
        await _cache.RemoveAsync("items_*");
        await _cache.RemoveAsync("stats_*");
        await _cache.RemoveAsync("recent_*");
    }

    // Called on user logout
    public async Task OnUserLogout(string userId)
    {
        // Clear all user-specific caches
        await _cache.RemoveAsync($"resume_{userId}");
        await _cache.RemoveAsync($"userdata_{userId}_*");
    }
}
```

### Manual Cache Control

```csharp
// Add endpoint to clear cache (admin only)
[Authorize(Roles = "Admin")]
[HttpPost("Admin/Cache/Clear")]
public async Task<IActionResult> ClearCache([FromQuery] string? pattern)
{
    _cacheInvalidationService.ClearPattern(pattern ?? "*");
    return Ok(new { message = "Cache cleared" });
}
```

---

## 4. Caching Configuration

### appsettings.json Configuration

```json
{
  "Caching": {
    "Enabled": true,
    "Strategy": "HttpResponseCache",  // or "DistributedCache" or "InMemory"
    "DefaultDuration": 30,            // seconds
    "PerEndpoint": {
      "GetItems": { "Duration": 30, "VaryByQueryParams": ["userId"] },
      "GetResumeItems": { "Duration": 30, "VaryByQueryParams": ["userId"] },
      "GetLibraryStats": { "Duration": 300 },
      "GetCollections": { "Duration": 600 }
    },
    "RedisConnection": "localhost:6379"  // if using DistributedCache
  }
}
```

### Startup Configuration

```csharp
public void ConfigureServices(IServiceCollection services)
{
    var cachingConfig = configuration.GetSection("Caching");
    
    if (cachingConfig.GetValue<bool>("Enabled"))
    {
        var strategy = cachingConfig.GetValue<string>("Strategy");
        
        switch (strategy)
        {
            case "HttpResponseCache":
                services.AddResponseCaching();
                break;
                
            case "DistributedCache":
                services.AddStackExchangeRedisCache(options =>
                {
                    options.Configuration = cachingConfig.GetValue<string>("RedisConnection");
                });
                break;
                
            case "InMemory":
            default:
                services.AddMemoryCache();
                break;
        }
    }
}

public void Configure(IApplicationBuilder app)
{
    if (configuration.GetValue<bool>("Caching:Enabled"))
    {
        app.UseResponseCaching();
    }
}
```

---

## 5. Implementation Roadmap

### Phase 1: Immediate (Quick Wins)
**Effort: 1-2 hours**
**Impact: 40-50% query reduction**

```csharp
// Add to these endpoints:
[ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any)]
public ItemsResult GetItems(...) { }

[ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any)]
public ItemsResult GetResumeItems(...) { }

[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public ItemsResult GetLatestItems(...) { }

[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]
public ItemsResult GetNextUpSeries(...) { }
```

### Phase 2: Distributed Caching (5-10 minutes)
**Effort: 3-4 hours**
**Impact: Additional 30-40% from multi-instance scenarios**

- Add Redis dependency
- Implement IDistributedCache wrapper
- Add cache invalidation on item changes

### Phase 3: Smart Invalidation (10-30 minutes)
**Effort: 4-6 hours**
**Impact: Confidence in cache correctness**

- Event-based invalidation
- Admin cache control UI
- Cache statistics dashboard

---

## 6. Performance Projections

### Scenario: Home Page Load (4 concurrent API calls)

**Without Caching (Current N+1 Fix):**
```
4 calls × 3 queries each = 12 queries
Total DB Time: ~150ms
Page Load Time: ~200ms
```

**With HTTP Response Caching:**
```
First load: 12 queries
Subsequent loads (within 30s): 0 queries (cache hit)
Cache Hit Rate: ~95% on home page (most users load within 30s)
Effective DB Queries: ~0.6 per page load
```

**With Distributed Cache + Invalidation:**
```
Server 1: 12 queries (generates cache)
Server 2: 0 queries (reads cache)
Server 3: 0 queries (reads cache)
Shared Rate: ~2-3 queries per 100 page loads
Cache Hit Rate: ~98%
```

---

## 7. Monitoring Cache Effectiveness

### Metrics to Track

```csharp
public class CacheMetrics
{
    public long TotalRequests { get; set; }
    public long CacheHits { get; set; }
    public long CacheMisses { get; set; }
    public long BytesSaved { get; set; }
    public double HitRate => CacheHits / (double)(CacheHits + CacheMisses);
    
    public TimeSpan AverageQueryTime { get; set; }
    public TimeSpan AverageCacheLookupTime { get; set; }
}
```

### Logging Cache Performance

```bash
# Monitor cache hits/misses
grep "CacheHit\|CacheMiss" /var/log/jellyfin/log_*.log | tail -100

# Measure impact
Before: $(grep "SELECT" log_before.log | wc -l) queries
After:  $(grep "SELECT" log_after.log | wc -l) queries
Improvement: $((100 * (before - after) / before))%
```

---

## 8. Testing Response Caching

### Manual Test

```bash
#!/bin/bash

# First request - cache miss
echo "First request (cache miss):"
time curl -s "http://localhost:8096/Items?userId=XXX" > /dev/null

# Second request - cache hit (should be instant)
echo "Second request (cache hit):"
time curl -s "http://localhost:8096/Items?userId=XXX" > /dev/null

# After 30+ seconds - cache expired
echo "Waiting 31 seconds..."
sleep 31

echo "Third request (cache miss after expiration):"
time curl -s "http://localhost:8096/Items?userId=XXX" > /dev/null
```

### Automated Test Suite

```csharp
[TestClass]
public class CachingTests
{
    [TestMethod]
    public void TestHttpResponseCaching()
    {
        // Verify Cache-Control headers are set
        // Verify cache hit on second request
        // Verify cache miss after expiration
    }

    [TestMethod]
    public void TestCacheInvalidation()
    {
        // Add item -> verify cache cleared
        // Delete item -> verify cache cleared
        // Modify item -> verify cache cleared
    }
}
```

---

## 9. Recommendations

### For This Project
1. ✅ Start with HTTP Response Caching (Phase 1)
   - Easiest to implement
   - Works immediately
   - No infrastructure changes

2. 🔄 Plan Phase 2 if multi-server deployment is planned

3. 🎯 Add cache invalidation once caching is active

### Suggested Implementation Order
1. **Week 1**: Deploy N+1 batching fix (already done ✅)
2. **Week 2**: Add HTTP response caching (Phase 1)
3. **Week 3-4**: Add cache invalidation + monitoring
4. **Week 5+**: Plan Phase 2 distributed caching if needed

---

## Summary

**Combined with N+1 Batching:**
- N+1 fixes: 87% reduction in queries
- Response caching: 90%+ cache hit rate on home page
- **Total: 97% reduction in queries for typical home page load**

**Recommended Next Step:**
Implement Phase 1 response caching by adding `[ResponseCache(...)]` attributes to frequently-used API endpoints (estimated 1-2 hours of work).
