fix(session): tie directory ownership to the live connection

Ownership is claimed with a Lua check-and-set keyed on the instance holding
the websocket, routing prefers a live controller over a local copy, the
session list deduplicates by owner, removal is ownership-checked, undelivered
routed messages surface, single-session lookups stop scanning the keyspace and
directory writes leave the request path bounded by a timeout.
This commit is contained in:
2026-09-24 23:50:01 +10:00
parent 6f362c33c9
commit 9e66708d87
17 changed files with 613 additions and 123 deletions
@@ -1,10 +1,13 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session;
@@ -15,6 +18,7 @@ namespace Emby.Server.Implementations.Session;
public sealed class RemoteSessionController : ISessionController
{
private readonly IPodMessageBus _bus;
private readonly ILogger _logger;
private readonly string _ownerPod;
private readonly string _sessionId;
@@ -22,27 +26,27 @@ public sealed class RemoteSessionController : ISessionController
/// Initializes a new instance of the <see cref="RemoteSessionController"/> class.
/// </summary>
/// <param name="bus">The cross-instance bus.</param>
/// <param name="logger">The logger.</param>
/// <param name="ownerPod">The instance holding the connection.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="isSessionActive">Whether the owner reported the session as active.</param>
/// <param name="supportsMediaControl">Whether the owner reported the session as controllable.</param>
public RemoteSessionController(IPodMessageBus bus, string ownerPod, string sessionId, bool isSessionActive, bool supportsMediaControl)
public RemoteSessionController(IPodMessageBus bus, ILogger logger, string ownerPod, string sessionId, bool supportsMediaControl)
{
_bus = bus;
_logger = logger;
_ownerPod = ownerPod;
_sessionId = sessionId;
IsSessionActive = isSessionActive;
SupportsMediaControl = supportsMediaControl;
}
/// <inheritdoc />
public bool IsSessionActive { get; }
public bool IsSessionActive => true;
/// <inheritdoc />
public bool SupportsMediaControl { get; }
/// <inheritdoc />
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
public async Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{
var routed = new RoutedSessionMessage
{
@@ -52,14 +56,21 @@ public sealed class RemoteSessionController : ISessionController
Data = JsonSerializer.Serialize(data, JsonDefaults.Options)
};
_bus.Publish(
var delivered = await _bus.PublishAsync(
_ownerPod,
new PodMessage
{
Kind = RoutedSessionMessage.Kind,
Payload = JsonSerializer.Serialize(routed, JsonDefaults.Options)
});
},
cancellationToken).ConfigureAwait(false);
return Task.CompletedTask;
if (delivered == 0)
{
_logger.LogWarning("Instance {OwnerPod} holds session {SessionId} but is not listening; the {MessageType} message was not delivered.", _ownerPod, _sessionId, name);
throw new ResourceNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The instance holding session {0} is unreachable.", _sessionId));
}
}
}