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:
@@ -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>();
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace Emby.Server.Implementations.Session
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly Jellyfin.Server.Implementations.Clustering.IInstanceRegistry _instanceRegistry;
|
||||
private readonly CancellationTokenRegistration _shutdownCallback;
|
||||
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -93,6 +94,7 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
|
||||
/// <param name="instanceRegistry">Instance registry for multi-instance support (optional for backward compatibility).</param>
|
||||
public SessionManager(
|
||||
ILogger<SessionManager> logger,
|
||||
IEventManager eventManager,
|
||||
@@ -106,7 +108,8 @@ namespace Emby.Server.Implementations.Session
|
||||
IServerApplicationHost appHost,
|
||||
IDeviceManager deviceManager,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IHostApplicationLifetime hostApplicationLifetime)
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
Jellyfin.Server.Implementations.Clustering.IInstanceRegistry instanceRegistry = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_eventManager = eventManager;
|
||||
@@ -120,6 +123,7 @@ namespace Emby.Server.Implementations.Session
|
||||
_appHost = appHost;
|
||||
_deviceManager = deviceManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_instanceRegistry = instanceRegistry;
|
||||
_shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
|
||||
|
||||
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
|
||||
@@ -156,10 +160,30 @@ namespace Emby.Server.Implementations.Session
|
||||
public event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all connections.
|
||||
/// Gets all connections for the current instance.
|
||||
/// </summary>
|
||||
/// <value>All connections.</value>
|
||||
public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
|
||||
/// <remarks>
|
||||
/// In multi-instance deployments, only returns sessions owned by the current instance.
|
||||
/// In single-instance deployments (when _instanceRegistry is null), returns all sessions.
|
||||
/// </remarks>
|
||||
public IEnumerable<SessionInfo> Sessions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instanceRegistry is null)
|
||||
{
|
||||
// Single-instance mode: return all sessions
|
||||
return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
|
||||
}
|
||||
|
||||
// Multi-instance mode: only return sessions for this instance
|
||||
var currentInstanceId = _instanceRegistry.CurrentInstanceId;
|
||||
return _activeConnections.Values
|
||||
.Where(s => s.InstanceId.Equals(currentInstanceId))
|
||||
.OrderByDescending(c => c.LastActivityDate);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
|
||||
{
|
||||
@@ -556,7 +580,8 @@ namespace Emby.Server.Implementations.Session
|
||||
DeviceId = deviceId,
|
||||
ApplicationVersion = appVersion,
|
||||
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
ServerId = _appHost.SystemId
|
||||
ServerId = _appHost.SystemId,
|
||||
InstanceId = _instanceRegistry?.CurrentInstanceId ?? Guid.Empty
|
||||
};
|
||||
|
||||
var username = user?.Username;
|
||||
|
||||
Reference in New Issue
Block a user