# Phase 4: Cache Coordination - COMPLETE ✅ **Date:** March 5, 2026 **Status:** Implementation Complete **Build Status:** ✅ Passed (all Phase 4 code compiles successfully) ## Overview Phase 4 implements cross-instance cache coordination using PostgreSQL's LISTEN/NOTIFY pub/sub mechanism. This ensures that when one Jellyfin instance modifies data, all other instances immediately invalidate their caches, preventing stale data from being served to users. ## Architecture ### PostgreSQL LISTEN/NOTIFY Pattern PostgreSQL's native pub/sub messaging system enables real-time notifications between instances without polling overhead: ``` Instance A: Updates item → Sends NOTIFY on channel ↓ PostgreSQL: Broadcasts notification ↓ Instance B & C: Receive notification → Invalidate cache ``` ### Components Implemented #### 1. Cache Message Types **File:** `Jellyfin.Server.Implementations/Clustering/CacheInvalidationMessage.cs` Defines the message format for cache invalidation events: ```csharp public enum CacheInvalidationType { Item, // Item metadata changed UserData, // User watch status, ratings, favorites Image, // Image updated ChapterImage, // Chapter image updated Metadata, // Metadata providers updated All, // Global cache clear Library, // Library structure changed Person, // Person/actor information changed User, // User profile changed Device // Device registration changed } public class CacheInvalidationMessage { public CacheInvalidationType Type { get; set; } public Guid? EntityId { get; set; } public string? CacheKey { get; set; } public DateTime Timestamp { get; set; } public Guid SourceInstanceId { get; set; } public Dictionary? Metadata { get; set; } } ``` #### 2. PostgreSQL Notification Listener **Interface:** `Jellyfin.Server.Implementations/Clustering/IPostgresNotificationListener.cs` **Implementation:** `Jellyfin.Server.Implementations/Clustering/PostgresNotificationListener.cs` Manages dedicated PostgreSQL connection for LISTEN/NOTIFY: **Key Features:** - **Dedicated Connection:** Separate from EF Core DbContext for persistent listening - **Background Task:** Continuous loop calling `NpgsqlConnection.WaitAsync()` - **Event-Driven:** Raises `NotificationReceived` event when messages arrive - **SQL Injection Protection:** Escapes single quotes in payloads - **Lifecycle Management:** Proper async disposal and cancellation **Usage:** ```csharp await _listener.StartListeningAsync("jellyfin_cache_invalidation"); _listener.NotificationReceived += OnNotificationReceived; // Send notification await _listener.NotifyAsync("jellyfin_cache_invalidation", jsonPayload); // Cleanup await _listener.StopListeningAsync(); ``` #### 3. Cache Coordinator **Interface:** `Jellyfin.Server.Implementations/Clustering/ICacheCoordinator.cs` **Implementation:** `Jellyfin.Server.Implementations/Clustering/CacheCoordinator.cs` Coordinates cache invalidation across all instances: **Key Features:** - **Message Broadcasting:** Sends invalidation messages via PostgreSQL NOTIFY - **Self-Filtering:** Ignores messages from the same instance (SourceInstanceId check) - **Typed Invalidation:** Separate methods for each cache type - **JSON Serialization:** Uses System.Text.Json for message encoding - **Extensible:** TODO markers for actual cache integration **Methods:** ```csharp Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); Task InvalidateItemAsync(Guid itemId, CancellationToken cancellationToken = default); Task InvalidateUserDataAsync(Guid userId, CancellationToken cancellationToken = default); Task InvalidateImageAsync(Guid itemId, CancellationToken cancellationToken = default); Task InvalidateLibraryAsync(Guid libraryId, CancellationToken cancellationToken = default); Task InvalidatePersonAsync(Guid personId, CancellationToken cancellationToken = default); Task InvalidateMetadataAsync(Guid itemId, CancellationToken cancellationToken = default); Task InvalidateAllAsync(CacheInvalidationType type, CancellationToken cancellationToken = default); ``` **Message Flow:** 1. Service calls `InvalidateItemAsync(itemId)` 2. CacheCoordinator creates `CacheInvalidationMessage` with current InstanceId 3. Message serialized to JSON 4. PostgreSQL NOTIFY sent on "jellyfin_cache_invalidation" channel 5. All listening instances receive notification 6. Each instance checks SourceInstanceId (skip if same) 7. ProcessInvalidationMessage processes the cache type 8. Actual cache invalidation occurs (TODO: integrate with cache managers) ## Service Registration **File:** `Emby.Server.Implementations/ApplicationHost.cs` Added to dependency injection container: ```csharp // Service registrations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); // Startup integration in RunStartupTasksAsync await StartCacheCoordinatorAsync().ConfigureAwait(false); private async Task StartCacheCoordinatorAsync() { var cacheCoordinator = Resolve(); await cacheCoordinator.StartAsync(CancellationToken.None).ConfigureAwait(false); _logger.LogInformation("Cache coordinator started successfully"); } ``` ## Build Fixes Applied During implementation, the following code quality issues were resolved: 1. **SA1201:** Moved `CacheInvalidationType` enum before class (member ordering) 2. **SA1028:** Removed trailing whitespace from multiple locations 3. **CA1307:** Added `StringComparison.Ordinal` to `string.Replace()` calls 4. **IDISP013:** Changed `Task.Run(() => ListenLoopAsync())` to direct `ListenLoopAsync()` assignment **Final Build Status:** ✅ All Phase 4 code compiles successfully ## Message Channel **Channel Name:** `jellyfin_cache_invalidation` All instances listen and send on this single channel. Message type differentiation happens via the `CacheInvalidationType` enum in the JSON payload. ## Testing Phase 4 ### Prerequisites 1. Apply database migration: `sql/add_multi_instance_support.sql` 2. Start multiple Jellyfin instances with `EnableMultiInstance=true` ### Manual Test 1. Start Instance A and Instance B 2. On Instance A, update an item (e.g., change metadata) 3. Verify Instance A sends NOTIFY (check logs) 4. Verify Instance B receives notification (check logs) 5. Verify Instance B invalidates its cache (check logs) ### Log Messages to Watch ``` Started listening on PostgreSQL channel: jellyfin_cache_invalidation Sent notification on channel jellyfin_cache_invalidation Received notification on channel jellyfin_cache_invalidation: {...} Cache invalidated for type: Item, EntityId: ``` ## Integration Points (TODO - Future Work) Phase 4 provides the infrastructure but needs integration with actual cache invalidation: ### 1. Library Manager Integration Hook into `LibraryManager` when items are added/updated/deleted: ```csharp public async Task UpdateItemAsync(BaseItem item, ...) { // Update database await _itemRepository.SaveItemAsync(item); // Notify other instances await _cacheCoordinator.InvalidateItemAsync(item.Id); } ``` ### 2. User Data Manager Integration Hook into `UserDataManager` when user watch status changes: ```csharp public async Task SaveUserDataAsync(Guid userId, ...) { // Save to database await _userDataRepository.SaveUserDataAsync(userId, userData); // Notify other instances await _cacheCoordinator.InvalidateUserDataAsync(userId); } ``` ### 3. Image Processor Integration Hook into `ImageProcessor` when images are generated/updated: ```csharp public async Task ProcessImageAsync(Guid itemId, ...) { // Process and save image await SaveImageAsync(itemId, image); // Notify other instances await _cacheCoordinator.InvalidateImageAsync(itemId); } ``` ### 4. Metadata Provider Integration Hook into metadata providers when external metadata is refreshed: ```csharp public async Task RefreshMetadataAsync(Guid itemId, ...) { // Fetch and save metadata await _metadataService.RefreshAsync(itemId); // Notify other instances await _cacheCoordinator.InvalidateMetadataAsync(itemId); } ``` ## Performance Considerations ### Why PostgreSQL LISTEN/NOTIFY is Efficient 1. **No Polling:** Database pushes notifications immediately 2. **Low Overhead:** Minimal network traffic (only when changes occur) 3. **Native Feature:** No additional infrastructure (Redis, RabbitMQ) 4. **Transactional:** Notifications can be part of database transactions 5. **Scalable:** PostgreSQL handles message distribution efficiently ### Message Frequency Cache invalidation messages are only sent when data actually changes: - Item updates: ~10-100/minute during library scans - User data: ~1-10/minute during active playback - Images: ~10-50/minute during library scans - Metadata: ~5-20/minute during refresh operations ### Network Impact Average message size: ~200-300 bytes JSON - 100 messages/minute = ~30 KB/minute - 1000 messages/minute = ~300 KB/minute Negligible impact on database performance. ## Security Considerations 1. **SQL Injection Protection:** Payloads are escaped (single quotes doubled) 2. **Instance Verification:** SourceInstanceId prevents spoofing (if needed) 3. **Channel Isolation:** All instances use the same channel (trusted environment) 4. **No Sensitive Data:** Messages contain only IDs and cache keys ## Known Limitations 1. **Cache Integration Not Complete:** TODO markers indicate where actual cache invalidation should occur 2. **No Message Ordering Guarantee:** PostgreSQL NOTIFY doesn't guarantee delivery order 3. **No Delivery Acknowledgment:** Fire-and-forget model (acceptable for cache invalidation) 4. **Single Channel:** All message types share one channel (could be split if needed) ## What's Next ### Immediate Next Steps (Phase 5) - **Primary Instance Election:** Coordinate scheduled tasks across instances - See: `docs/MULTI_INSTANCE_SUPPORT_PLAN.md` Phase 5 section ### Future Enhancements - Integrate cache invalidation with actual cache managers - Add performance metrics (messages sent/received per instance) - Add cache invalidation rate limiting if needed - Consider message batching for high-frequency updates ## Related Files ### Phase 4 Implementation - `Jellyfin.Server.Implementations/Clustering/CacheInvalidationMessage.cs` - `Jellyfin.Server.Implementations/Clustering/IPostgresNotificationListener.cs` - `Jellyfin.Server.Implementations/Clustering/PostgresNotificationListener.cs` - `Jellyfin.Server.Implementations/Clustering/PostgresNotificationEventArgs.cs` - `Jellyfin.Server.Implementations/Clustering/ICacheCoordinator.cs` - `Jellyfin.Server.Implementations/Clustering/CacheCoordinator.cs` - `Emby.Server.Implementations/ApplicationHost.cs` (service registration) ### Previous Phases - Phase 1: `docs/MULTI_INSTANCE_SUPPORT_SUMMARY.md` - Phase 2: `docs/MULTI_INSTANCE_SUPPORT_SUMMARY.md` - Phase 3: `docs/PHASE3_SESSION_ISOLATION_COMPLETE.md` ### Architecture - `docs/MULTI_INSTANCE_SUPPORT_PLAN.md` (complete architecture) - `docs/MULTI_INSTANCE_QUICKSTART.md` (setup guide) ## Summary ✅ **Phase 4 Complete!** - PostgreSQL LISTEN/NOTIFY infrastructure implemented - Cache message types defined - Notification listener with background task - Cache coordinator with typed invalidation methods - Service registration and startup integration - All code compiles successfully - Ready for runtime testing and integration **Current Progress:** 4 of 6 phases complete (67%) **Next:** Phase 5 - Primary Instance Election for coordinating scheduled tasks