From 4dd8a7801fcc2fa888bdde10d2d3ebd1bfe027ac Mon Sep 17 00:00:00 2001 From: madelson <1269046+madelson@users.noreply.github.com> Date: Sat, 17 Dec 2022 17:44:50 -0500 Subject: [PATCH] Ensure that OptionsCache only permits creating a single options instance per name (#79639) fix https://github.com/dotnet/runtime/issues/79529 --- .../src/OptionsCache.cs | 2 +- .../OptionsMonitorTest.cs | 52 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs index 9be845c..51de2bb 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs +++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsCache.cs @@ -65,7 +65,7 @@ namespace Microsoft.Extensions.Options #if NET || NETSTANDARD2_1 return _cache.GetOrAdd( name ?? Options.DefaultName, - static (name, arg) => new Lazy(arg.createOptions(name, arg.factoryArgument)), (createOptions, factoryArgument)).Value; + static (name, arg) => new Lazy(() => arg.createOptions(name, arg.factoryArgument)), (createOptions, factoryArgument)).Value; #endif } diff --git a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs index 06fb1d9..c04f48a 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsMonitorTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; @@ -485,5 +486,56 @@ namespace Microsoft.Extensions.Options.Tests Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - initialBytes); } #endif + + /// + /// Replicates https://github.com/dotnet/runtime/issues/79529 + /// + [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "Synchronous wait is not supported on browser")] + public void InstantiatesOnlyOneOptionsInstance() + { + using AutoResetEvent @event = new(initialState: false); + + OptionsMonitor monitor = new( + // WaitHandleConfigureOptions makes instance configuration slow enough to force a race condition + new OptionsFactory(new[] { new WaitHandleConfigureOptions(@event) }, Enumerable.Empty>()), + Enumerable.Empty>(), + new OptionsCache()); + + using Barrier barrier = new(participantCount: 2); + Task[] instanceTasks = Enumerable.Range(0, 2) + .Select(_ => Task.Factory.StartNew( + () => + { + barrier.SignalAndWait(); + return monitor.Get("someName"); + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default) + ) + .ToArray(); + + // No tasks can finish yet; but give them a chance to run and get blocked on the WaitHandle + Assert.Equal(-1, Task.WaitAny(instanceTasks, TimeSpan.FromSeconds(0.01))); + + // 1 release should be sufficient to complete both tasks + @event.Set(); + Assert.True(Task.WaitAll(instanceTasks, TimeSpan.FromSeconds(30))); + Assert.Equal(1, instanceTasks.Select(t => t.Result).Distinct().Count()); + } + + private class WaitHandleConfigureOptions : IConfigureNamedOptions + { + private readonly WaitHandle _waitHandle; + + public WaitHandleConfigureOptions(WaitHandle waitHandle) + { + _waitHandle = waitHandle; + } + + void IConfigureNamedOptions.Configure(string? name, FakeOptions options) => _waitHandle.WaitOne(); + void IConfigureOptions.Configure(FakeOptions options) => _waitHandle.WaitOne(); + } } } -- 2.7.4