Files
pgsql-jellyfin/docs/PHASE4_CACHE_COORDINATION_COMPLETE.md
wjones 77e30685bb Complete multi-instance support: Phases 3–6 & deployment
- Implements Phases 3–6: session isolation, cache coordination, primary election, and file system monitor coordination for Jellyfin with PostgreSQL.
- Adds new database entities (Instance, DistributedLock, FileSystemChange) and EF model configurations.
- Includes SQL migration scripts and EF migration for all required tables, columns, and helper functions.
- Updates Device entity and JellyfinDbContext for multi-instance tracking.
- Integrates new DI services for instance registry, distributed locks, cache coordinator, and primary election.
- Adds publishing profiles (Win/Linux/FrameworkDependent) and automation script for deployment.
- Extensive documentation for architecture, setup, and publishing.
- All changes are backward compatible and build successfully.
2026-03-05 16:10:26 -05:00

12 KiB

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:

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<string, string>? 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:

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:

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:

// Service registrations
serviceCollection.AddSingleton<IPostgresNotificationListener, PostgresNotificationListener>();
serviceCollection.AddSingleton<ICacheCoordinator, CacheCoordinator>();

// Startup integration in RunStartupTasksAsync
await StartCacheCoordinatorAsync().ConfigureAwait(false);

private async Task StartCacheCoordinatorAsync()
{
    var cacheCoordinator = Resolve<ICacheCoordinator>();
    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: <guid>

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:

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:

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:

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:

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

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