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
@@ -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;