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,5 +1,6 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Session;
@@ -16,6 +17,8 @@ public sealed class RedisPodMessageBus : IPodMessageBus
{
private const string ChannelPrefix = "jellyfin:pod:";
private static readonly TimeSpan _publishTimeout = TimeSpan.FromSeconds(5);
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly ISubscriber _subscriber;
@@ -39,7 +42,7 @@ public sealed class RedisPodMessageBus : IPodMessageBus
public string PodId { get; }
/// <inheritdoc />
public void Publish(string targetPod, PodMessage message)
public async Task<long> PublishAsync(string targetPod, PodMessage message, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(targetPod);
ArgumentNullException.ThrowIfNull(message);
@@ -48,14 +51,14 @@ public sealed class RedisPodMessageBus : IPodMessageBus
try
{
_subscriber.Publish(
return await _subscriber.PublishAsync(
RedisChannel.Literal(ChannelPrefix + targetPod),
JsonSerializer.Serialize(message, _jsonOptions),
CommandFlags.FireAndForget);
JsonSerializer.Serialize(message, _jsonOptions)).WaitAsync(_publishTimeout, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send a {Kind} message to {TargetPod}.", message.Kind, targetPod);
return 0;
}
}