Files
jellyfin-ha-src/Jellyfin.Server/Extensions/ScanLeaderServiceCollectionExtensions.cs
T
unkin-agent 91655fcb0a
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix(ha): gate library tasks on the scan leader by default
Jellyfin:ScanLeader:Enabled defaults to false and nothing sets it, so
leader election never runs and every replica executes the timer-driven
library tasks concurrently - the exact behaviour the lease prevents.

- Enable gating by default when a Redis connection string is configured
- Honour an explicit Enabled setting either way
- Carry the effective decision onto the bound options the task worker reads
- Log at startup whether gating is active
- Warn when gating is enabled but no Redis connection string is configured
2026-09-13 13:19:48 +10:00

72 lines
3.1 KiB
C#

using System;
using Emby.Server.Implementations.ScheduledTasks;
using MediaBrowser.Controller.ScheduledTasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Extensions;
/// <summary>
/// Extensions for registering the scan-leader lease.
/// </summary>
public static class ScanLeaderServiceCollectionExtensions
{
private const string RedisConnectionStringKey = "Jellyfin:TranscodeStore:RedisConnectionString";
/// <summary>
/// Registers the scan-leader lease and reports at <see cref="LogLevel.Information"/> whether
/// timer-driven library tasks are gated to a single instance.
/// </summary>
/// <remarks>
/// Gating is on by default once a Redis connection string is configured: that is only set for a
/// multi-instance deployment, which is the only shape where running library scans on every
/// instance is wrong. Single-instance installs have no Redis and keep running every task locally.
/// </remarks>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="configuration">The configuration to read <c>Jellyfin:ScanLeader</c> from.</param>
/// <param name="logger">The logger to report the gating decision on.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddScanLeaderLease(
this IServiceCollection serviceCollection,
IConfiguration configuration,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(logger);
serviceCollection.Configure<ScanLeaderOptions>(configuration.GetSection(ScanLeaderOptions.ConfigurationSection));
var redisConfigured = !string.IsNullOrEmpty(configuration[RedisConnectionStringKey]);
var requested = bool.TryParse(configuration[ScanLeaderOptions.EnabledKey], out var explicitChoice)
? explicitChoice
: redisConfigured;
var active = requested && redisConfigured;
// The task worker reads Enabled from the bound options, so the effective decision has to land
// there as well as on the lease registration below.
serviceCollection.PostConfigure<ScanLeaderOptions>(options => options.Enabled = active);
if (active)
{
logger.LogInformation(
"Scan-leader gating is active: timer-driven library tasks run only on the instance holding the Redis scan-leader lease.");
return serviceCollection.AddSingleton<IScanLeaderLease, RedisScanLeaderLease>();
}
if (requested)
{
logger.LogWarning(
"Scan-leader gating is enabled but no Redis connection string is configured ({Key}), so it cannot run: timer-driven library tasks run on every instance.",
RedisConnectionStringKey);
}
else
{
logger.LogInformation("Scan-leader gating is off: timer-driven library tasks run on every instance.");
}
return serviceCollection.AddSingleton<IScanLeaderLease, NullScanLeaderLease>();
}
}