Files
jellyfin-ha-src/Emby.Server.Implementations/Session/RemoteSessionController.cs
T
unkin-agent 9e66708d87 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.
2026-09-24 23:50:01 +10:00

77 lines
2.7 KiB
C#

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;
/// <summary>
/// Stands in for the websocket of a session another instance holds: messages are forwarded to that
/// instance, which writes them to the connection it owns.
/// </summary>
public sealed class RemoteSessionController : ISessionController
{
private readonly IPodMessageBus _bus;
private readonly ILogger _logger;
private readonly string _ownerPod;
private readonly string _sessionId;
/// <summary>
/// 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="supportsMediaControl">Whether the owner reported the session as controllable.</param>
public RemoteSessionController(IPodMessageBus bus, ILogger logger, string ownerPod, string sessionId, bool supportsMediaControl)
{
_bus = bus;
_logger = logger;
_ownerPod = ownerPod;
_sessionId = sessionId;
SupportsMediaControl = supportsMediaControl;
}
/// <inheritdoc />
public bool IsSessionActive => true;
/// <inheritdoc />
public bool SupportsMediaControl { get; }
/// <inheritdoc />
public async Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{
var routed = new RoutedSessionMessage
{
SessionId = _sessionId,
MessageType = name,
MessageId = messageId,
Data = JsonSerializer.Serialize(data, JsonDefaults.Options)
};
var delivered = await _bus.PublishAsync(
_ownerPod,
new PodMessage
{
Kind = RoutedSessionMessage.Kind,
Payload = JsonSerializer.Serialize(routed, JsonDefaults.Options)
},
cancellationToken).ConfigureAwait(false);
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));
}
}
}