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
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user