using System; using System.Text.Json; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using Microsoft.Extensions.Logging; using StackExchange.Redis; namespace Emby.Server.Implementations.Configuration { /// /// A Redis pub/sub . Notices are broadcast on one channel /// and every instance but the publisher applies them. /// public sealed class RedisConfigurationInvalidationBus : IConfigurationInvalidationBus { private const string ChannelName = "jellyfin:configinvalidation"; private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly ISubscriber _subscriber; private readonly ILogger _logger; private readonly string _originId; /// /// Initializes a new instance of the class. /// /// The Redis connection multiplexer. /// The logger. public RedisConfigurationInvalidationBus(IConnectionMultiplexer redis, ILogger logger) { ArgumentNullException.ThrowIfNull(redis); _subscriber = redis.GetSubscriber(); _logger = logger; _originId = Environment.GetEnvironmentVariable("JELLYFIN_INSTANCE_ID") ?? Environment.MachineName; } /// public void Publish(ConfigurationInvalidationScope scope, string? target) { var invalidation = new ConfigurationInvalidation { Scope = scope, Target = target, OriginId = _originId }; try { // Fire and forget: an admin saving configuration must never wait on, or fail because of, // the bus. The write has already reached the shared directory by this point. _subscriber.Publish( RedisChannel.Literal(ChannelName), JsonSerializer.Serialize(invalidation, _jsonOptions), CommandFlags.FireAndForget); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to publish {Scope} invalidation for {Target}; other instances keep their cached copy until they restart.", scope, target); } } /// public void Subscribe(Action handler) { ArgumentNullException.ThrowIfNull(handler); try { _subscriber.Subscribe(RedisChannel.Literal(ChannelName), (_, value) => Dispatch(handler, value)); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to subscribe to configuration invalidations; this instance keeps its cached configuration until it restarts."); } } private void Dispatch(Action handler, RedisValue value) { try { var invalidation = JsonSerializer.Deserialize(value.ToString(), _jsonOptions); if (invalidation is null || string.Equals(invalidation.OriginId, _originId, StringComparison.Ordinal)) { return; } handler(invalidation); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to apply a configuration invalidation."); } } } }