using System; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.ScheduledTasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StackExchange.Redis; namespace Emby.Server.Implementations.ScheduledTasks; /// /// A Redis-backed that elects a single scan-leader instance using a /// TTL lease on a shared key. The lease value is this instance's pod identity; a leader that keeps /// renewing retains the lease, and any instance can claim it once the previous leader's lease expires. /// public sealed class RedisScanLeaderLease : IScanLeaderLease { private const string LeaderKey = "jellyfin:scanleader"; /// /// Lua script for atomic acquire-or-renew: if the key is unset (missing or already expired) it is /// set to this pod for the lease duration and 1 is returned; if it already holds this pod the TTL is /// extended and 1 is returned; otherwise another pod owns a live lease and 0 is returned. /// private const string AcquireOrRenewScript = @" local current = redis.call('GET', KEYS[1]) if not current then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 elseif current == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 else return 0 end"; private readonly IDatabase _db; private readonly ScanLeaderOptions _options; private readonly ILogger _logger; private readonly string _podId; /// /// Initializes a new instance of the class. /// /// The Redis connection multiplexer. /// The scan-leader configuration options. /// The logger. public RedisScanLeaderLease( IConnectionMultiplexer redis, IOptions options, ILogger logger) { _db = redis.GetDatabase(); _options = options.Value; _logger = logger; _podId = Environment.GetEnvironmentVariable("JELLYFIN_INSTANCE_ID") ?? Environment.MachineName; } /// public async Task TryAcquireOrRenewAsync(CancellationToken cancellationToken = default) { var leaseDurationMs = (long)_options.LeaseDurationSeconds * 1000; try { var result = (long?)await _db.ScriptEvaluateAsync( AcquireOrRenewScript, keys: new RedisKey[] { LeaderKey }, values: new RedisValue[] { _podId, leaseDurationMs }).ConfigureAwait(false); return result == 1; } catch (Exception ex) { // Fail-safe: if Redis is unreachable, treat this instance as the leader so scheduled scans // keep running. Every instance scanning is preferable to no instance scanning. _logger.LogWarning(ex, "Scan-leader lease evaluation failed; treating {PodId} as leader.", _podId); return true; } } }