keep an applied invalidation from inducing a write that publishes back
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
2026-09-21 00:24:01 +10:00
parent b662ffa48f
commit a9d6c749fb
8 changed files with 312 additions and 6 deletions
@@ -0,0 +1,30 @@
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Extensions for <see cref="IConfigurationInvalidationBus"/>.
/// </summary>
public static class ConfigurationInvalidationBusExtensions
{
/// <summary>
/// Announces a write this instance originated, and stays silent for a write induced by an
/// invalidation another instance published.
/// </summary>
/// <remarks>
/// This is the backstop for the write path: a consumer reacting to an applied invalidation by
/// writing - including a plugin that knows nothing about the bus - cannot turn that write into a
/// notice of its own.
/// </remarks>
/// <param name="bus">The bus.</param>
/// <param name="scope">The cache that was written.</param>
/// <param name="target">The configuration key or library path that was written, if any.</param>
public static void PublishLocalWrite(this IConfigurationInvalidationBus bus, ConfigurationInvalidationScope scope, string? target)
{
if (ConfigurationInvalidationContext.IsApplyingRemoteInvalidation)
{
return;
}
bus.Publish(scope, target);
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Threading;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Marks the flow of control that is applying an invalidation published by another instance, so the
/// reactions to it can tell a remote write apart from a local one.
/// </summary>
/// <remarks>
/// Applying an invalidation raises the same update events a local save raises, because the in-process
/// consumers have to re-read the configuration either way. Some of those consumers answer an update by
/// writing, and that write must neither repeat what the publishing instance already did nor fan back
/// out over the bus. The flag rides the execution context, so it reaches the queued and asynchronous
/// event handlers as well as the synchronous ones.
/// </remarks>
public static class ConfigurationInvalidationContext
{
private static readonly AsyncLocal<bool> _applyingRemoteInvalidation = new();
/// <summary>
/// Gets a value indicating whether the current flow of control is applying an invalidation
/// published by another instance rather than handling a local save.
/// </summary>
public static bool IsApplyingRemoteInvalidation => _applyingRemoteInvalidation.Value;
/// <summary>
/// Marks the current flow of control as applying a remote invalidation until the returned scope is
/// disposed.
/// </summary>
/// <returns>The scope to dispose once the invalidation has been applied.</returns>
public static IDisposable BeginApply() => new ApplyScope();
private sealed class ApplyScope : IDisposable
{
private readonly bool _previous;
private bool _disposed;
public ApplyScope()
{
_previous = _applyingRemoteInvalidation.Value;
_applyingRemoteInvalidation.Value = true;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_applyingRemoteInvalidation.Value = _previous;
}
}
}
}