Files
unkinben 0008bde28e
ABI Compatibility / ABI - HEAD (pull_request) Has been cancelled
ABI Compatibility / ABI - BASE (pull_request) Has been cancelled
OpenAPI / OpenAPI - HEAD (pull_request) Has been cancelled
OpenAPI / OpenAPI - BASE (pull_request) Has been cancelled
Tests / run-phase5-tests (pull_request) Has been cancelled
Tests / run-tests (pull_request) Has been cancelled
Project Automation / Project board (pull_request) Has been cancelled
Merge Conflict Labeler / Labeling (pull_request) Has been cancelled
ABI Compatibility / ABI - Difference (pull_request) Has been cancelled
OpenAPI / OpenAPI - Difference (pull_request) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (pull_request) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (pull_request) Has been cancelled
Gate periodic library-mutating tasks behind a scan-leader lease
In a multi-pod deployment every pod runs the scheduled-task timers, so
periodic library-mutating tasks (library refresh, people/chapter refresh,
audio normalization, media-segment and keyframe extraction, collection and
user-data cleanup, database optimization) fire concurrently against the shared
database and library, duplicating work and racing each other.

Add an IScanLeaderLease abstraction that elects a single scan leader via a
Redis TTL lease keyed on the pod identity, mirroring the existing transcode
lease machinery. RedisScanLeaderLease acquires or renews the lease with an
atomic Lua script and fails safe by treating the pod as leader whenever Redis
is unreachable, so scans never stall. NullScanLeaderLease preserves the
single-instance behavior when election is disabled or no Redis connection is
configured.

Gate only the timer-driven path in ScheduledTaskWorker: when election is
enabled and a task key is in the gated set, a non-leader re-arms its trigger
and skips enqueueing. Manual and API-triggered runs bypass this path and still
run on any pod. Wiring is additive and the new worker constructor parameters
are optional, so existing behavior is unchanged when election is off.

Signed-off-by: Ben Vincent <ben@unkin.net>
2026-08-10 23:51:18 +10:00

177 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using Emby.Server.Implementations.MediaEncoding;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Session;
using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Database.Implementations;
using Jellyfin.Drawing;
using Jellyfin.Drawing.Skia;
using Jellyfin.LiveTv;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.ScheduledTasks;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity;
using MediaBrowser.Providers.Lyric;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Jellyfin.Server
{
/// <summary>
/// Implementation of the abstract <see cref="ApplicationHost" /> class.
/// </summary>
public class CoreAppHost : ApplicationHost
{
private readonly IConfiguration _startupConfig;
/// <summary>
/// Initializes a new instance of the <see cref="CoreAppHost" /> class.
/// </summary>
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig)
: base(
applicationPaths,
loggerFactory,
options,
startupConfig)
{
_startupConfig = startupConfig;
}
/// <inheritdoc/>
protected override void RegisterServices(IServiceCollection serviceCollection)
{
// Register an image encoder
bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
Type imageEncoderType = useSkiaEncoder
? typeof(SkiaEncoder)
: typeof(NullImageEncoder);
serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
// Log a warning if the Skia encoder could not be used
if (!useSkiaEncoder)
{
Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
}
serviceCollection.AddEventServices();
serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
serviceCollection.AddSingleton<IEventManager, EventManager>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>();
serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>();
serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>();
serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
serviceCollection.AddSingleton<IWebSocketListener, ActivityLogWebSocketListener>();
serviceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
serviceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
serviceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
// Transcode session store: Redis-backed when configured, no-op otherwise.
serviceCollection.Configure<TranscodeStoreOptions>(_startupConfig.GetSection("Jellyfin:TranscodeStore"));
var redisConnectionString = _startupConfig["Jellyfin:TranscodeStore:RedisConnectionString"];
if (!string.IsNullOrEmpty(redisConnectionString))
{
serviceCollection.AddSingleton<IConnectionMultiplexer>(sp =>
{
try
{
return ConnectionMultiplexer.Connect(redisConnectionString);
}
catch (Exception ex)
{
sp.GetRequiredService<ILogger<CoreAppHost>>()
.LogError(ex, "Failed to connect to Redis. Check the Jellyfin:TranscodeStore:RedisConnectionString configuration.");
throw;
}
});
serviceCollection.AddSingleton<ITranscodeSessionStore, RedisTranscodeSessionStore>();
}
else
{
serviceCollection.AddSingleton<ITranscodeSessionStore, NullTranscodeSessionStore>();
}
// Scan-leader lease: gates periodic library-mutating scheduled tasks to a single leader
// instance. Redis-backed when enabled and a Redis connection is configured, no-op otherwise.
serviceCollection.Configure<ScanLeaderOptions>(_startupConfig.GetSection("Jellyfin:ScanLeader"));
var scanLeaderEnabled = bool.TryParse(_startupConfig["Jellyfin:ScanLeader:Enabled"], out var enabled) && enabled;
if (scanLeaderEnabled && !string.IsNullOrEmpty(redisConnectionString))
{
serviceCollection.AddSingleton<IScanLeaderLease, RedisScanLeaderLease>();
}
else
{
serviceCollection.AddSingleton<IScanLeaderLease, NullScanLeaderLease>();
}
foreach (var type in GetExportTypes<ILyricProvider>())
{
serviceCollection.AddSingleton(typeof(ILyricProvider), type);
}
foreach (var type in GetExportTypes<ILyricParser>())
{
serviceCollection.AddSingleton(typeof(ILyricParser), type);
}
base.RegisterServices(serviceCollection);
}
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
// Jellyfin.Server
yield return typeof(CoreAppHost).Assembly;
// Jellyfin.Database.Implementations
yield return typeof(JellyfinDbContext).Assembly;
// Jellyfin.Server.Implementations
yield return typeof(ServiceCollectionExtensions).Assembly;
// Jellyfin.LiveTv
yield return typeof(LiveTvManager).Assembly;
}
}
}