propagate shared-config and library-option changes between instances
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

Add a Redis pub/sub invalidation bus, publish from the configuration and library-option write paths, and drop the matching local cache entry on the instances that did not write. No-op without a Redis connection string, and fails open when it is unreachable.
This commit is contained in:
2026-09-20 23:47:21 +10:00
parent ec581b5e5f
commit b662ffa48f
16 changed files with 919 additions and 1 deletions
@@ -0,0 +1,26 @@
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// A notice that one instance has written shared configuration, so every other instance has to drop
/// its locally cached copy and read the shared file again.
/// </summary>
public sealed class ConfigurationInvalidation
{
/// <summary>
/// Gets or sets the cache this notice refers to.
/// </summary>
public ConfigurationInvalidationScope Scope { get; set; }
/// <summary>
/// Gets or sets what was invalidated within the scope: the configuration key for
/// <see cref="ConfigurationInvalidationScope.NamedConfiguration"/>, the library path for
/// <see cref="ConfigurationInvalidationScope.LibraryOptions"/>, and <c>null</c> otherwise.
/// </summary>
public string? Target { get; set; }
/// <summary>
/// Gets or sets the identity of the instance that published the notice, so it can ignore its own.
/// </summary>
public string? OriginId { get; set; }
}
}
@@ -0,0 +1,29 @@
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Identifies which locally cached copy of the shared configuration a
/// <see cref="ConfigurationInvalidation"/> refers to.
/// </summary>
public enum ConfigurationInvalidationScope
{
/// <summary>
/// The system configuration cached by the configuration manager.
/// </summary>
SystemConfiguration = 0,
/// <summary>
/// A single named configuration, identified by its key.
/// </summary>
NamedConfiguration = 1,
/// <summary>
/// The library options of a single collection folder, identified by its path.
/// </summary>
LibraryOptions = 2,
/// <summary>
/// The library options of every collection folder, for changes to the library structure itself.
/// </summary>
AllLibraryOptions = 3
}
}
@@ -0,0 +1,28 @@
using System;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Carries cache-invalidation notices between the instances that share one configuration directory.
/// </summary>
/// <remarks>
/// The shared directory carries the content; this bus only carries the fact that it changed. Every
/// implementation is expected to fail open: a bus that cannot deliver must not throw into the write
/// path, leaving each instance on its own locally cached copy until it restarts.
/// </remarks>
public interface IConfigurationInvalidationBus
{
/// <summary>
/// Announces that this instance has written shared configuration.
/// </summary>
/// <param name="scope">The cache that was written.</param>
/// <param name="target">The configuration key or library path that was written, if any.</param>
void Publish(ConfigurationInvalidationScope scope, string? target);
/// <summary>
/// Registers the handler invoked for notices published by other instances.
/// </summary>
/// <param name="handler">The handler applying the invalidation locally.</param>
void Subscribe(Action<ConfigurationInvalidation> handler);
}
}
@@ -85,6 +85,13 @@ namespace MediaBrowser.Common.Configuration
/// </summary>
/// <param name="factories">The factories.</param>
void AddParts(IEnumerable<IConfigurationFactory> factories);
/// <summary>
/// Drops the locally cached copy of configuration another instance has written to the shared
/// configuration directory, so the next read reloads it, and raises the local update event.
/// </summary>
/// <param name="key">The named configuration key, or <c>null</c> for the system configuration.</param>
void InvalidateCachedConfiguration(string? key);
}
public static class ConfigurationManagerExtensions
@@ -0,0 +1,27 @@
using System;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// A no-op <see cref="IConfigurationInvalidationBus"/> used by single-instance installs and whenever
/// the shared bus is unavailable. Every instance keeps its own cached configuration, which is the
/// behaviour of an install that has only one.
/// </summary>
public sealed class NullConfigurationInvalidationBus : IConfigurationInvalidationBus
{
/// <summary>
/// Gets the shared instance.
/// </summary>
public static NullConfigurationInvalidationBus Instance { get; } = new NullConfigurationInvalidationBus();
/// <inheritdoc />
public void Publish(ConfigurationInvalidationScope scope, string? target)
{
}
/// <inheritdoc />
public void Subscribe(Action<ConfigurationInvalidation> handler)
{
}
}
}