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.
This commit is contained in:
2026-03-05 16:10:26 -05:00
parent 7eb2b445cb
commit 77e30685bb
52 changed files with 9349 additions and 22 deletions
+77 -3
View File
@@ -16,6 +16,7 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Emby.Photos;
@@ -409,7 +410,7 @@ namespace Emby.Server.Implementations
/// Runs the startup tasks.
/// </summary>
/// <returns><see cref="Task" />.</returns>
public Task RunStartupTasksAsync()
public async Task RunStartupTasksAsync()
{
Logger.LogInformation("Running startup tasks");
@@ -426,10 +427,64 @@ namespace Emby.Server.Implementations
}
Logger.LogInformation("ServerId: {ServerId}", SystemId);
// Register this instance in multi-instance mode
await RegisterInstanceAsync().ConfigureAwait(false);
// Start cache coordinator for multi-instance cache invalidation
await StartCacheCoordinatorAsync().ConfigureAwait(false);
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
}
return Task.CompletedTask;
private async Task RegisterInstanceAsync()
{
try
{
var instanceRegistry = Resolve<Jellyfin.Server.Implementations.Clustering.IInstanceRegistry>();
var networkConfig = ConfigurationManager.GetNetworkConfiguration();
var hostname = Environment.MachineName;
var processId = Environment.ProcessId;
var httpPort = networkConfig.InternalHttpPort;
var httpsPort = networkConfig.InternalHttpsPort > 0 ? networkConfig.InternalHttpsPort : (int?)null;
var version = ApplicationVersionString;
await instanceRegistry.RegisterInstanceAsync(
hostname,
processId,
httpPort,
httpsPort,
version,
capabilities: null,
CancellationToken.None).ConfigureAwait(false);
Logger.LogInformation(
"Instance registered: {InstanceId} on {Hostname}:{HttpPort}",
instanceRegistry.CurrentInstanceId,
hostname,
httpPort);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to register instance in database");
}
}
private async Task StartCacheCoordinatorAsync()
{
try
{
var cacheCoordinator = Resolve<Jellyfin.Server.Implementations.Clustering.ICacheCoordinator>();
await cacheCoordinator.StartAsync(CancellationToken.None).ConfigureAwait(false);
Logger.LogInformation("Cache coordinator started for multi-instance cache synchronization");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to start cache coordinator");
}
}
/// <inheritdoc/>
@@ -492,7 +547,16 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(NetManager);
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
// Register the actual TaskManager
serviceCollection.AddSingleton<TaskManager>();
// Wrap it with primary instance checking decorator
serviceCollection.AddSingleton<ITaskManager>(provider =>
{
var taskManager = provider.GetRequiredService<TaskManager>();
var primaryElection = provider.GetRequiredService<Jellyfin.Server.Implementations.Clustering.IPrimaryElectionService>();
var logger = provider.GetRequiredService<ILogger<Jellyfin.Server.Implementations.Clustering.PrimaryInstanceTaskManager>>();
return new Jellyfin.Server.Implementations.Clustering.PrimaryInstanceTaskManager(taskManager, primaryElection, logger);
});
serviceCollection.AddSingleton(_xmlSerializer);
@@ -554,6 +618,16 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
serviceCollection.AddSingleton<IDtoService, DtoService>();
// Multi-instance support services
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.IInstanceRegistry, Jellyfin.Server.Implementations.Clustering.InstanceRegistry>();
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.IDistributedLockManager, Jellyfin.Server.Implementations.Clustering.DistributedLockManager>();
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.IPostgresNotificationListener, Jellyfin.Server.Implementations.Clustering.PostgresNotificationListener>();
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.ICacheCoordinator, Jellyfin.Server.Implementations.Clustering.CacheCoordinator>();
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.IPrimaryElectionService, Jellyfin.Server.Implementations.Clustering.PrimaryElectionService>();
serviceCollection.AddHostedService(provider => (Jellyfin.Server.Implementations.Clustering.PrimaryElectionService)provider.GetRequiredService<Jellyfin.Server.Implementations.Clustering.IPrimaryElectionService>());
serviceCollection.AddSingleton<Jellyfin.Server.Implementations.Clustering.IFileSystemChangeProcessor, Jellyfin.Server.Implementations.Clustering.FileSystemChangeProcessor>();
serviceCollection.AddHostedService(provider => (Jellyfin.Server.Implementations.Clustering.FileSystemChangeProcessor)provider.GetRequiredService<Jellyfin.Server.Implementations.Clustering.IFileSystemChangeProcessor>());
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();