91655fcb0a
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
72 lines
3.1 KiB
C#
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>();
|
|
}
|
|
}
|