Fix CompositeChangeToken & CancellationTokenSource deadlock (#90078)
authorRik Steenkamp <c5fstn@ewps.nl>
Sun, 6 Aug 2023 17:07:56 +0000 (19:07 +0200)
committerGitHub <noreply@github.com>
Sun, 6 Aug 2023 17:07:56 +0000 (10:07 -0700)
src/libraries/Microsoft.Extensions.Primitives/src/CompositeChangeToken.cs
src/libraries/Microsoft.Extensions.Primitives/tests/CompositeChangeTokenTest.cs
src/libraries/Microsoft.Extensions.Primitives/tests/Microsoft.Extensions.Primitives.Tests.csproj

index edfe38b..a677965 100644 (file)
@@ -131,6 +131,11 @@ namespace Microsoft.Extensions.Primitives
 
             lock (compositeChangeTokenState._callbackLock)
             {
+                if (compositeChangeTokenState._cancellationTokenSource.IsCancellationRequested)
+                {
+                    return;
+                }
+
                 try
                 {
                     compositeChangeTokenState._cancellationTokenSource.Cancel();
index ffb66ad..b3718cf 100644 (file)
@@ -170,6 +170,28 @@ namespace Microsoft.Extensions.Primitives
             // Assert
             Assert.Equal(1, count);
         }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public async Task NoDeadlock_WhenMultipleConcurrentChangeEventsOccur()
+        {
+            // Arrange
+            var firstCancellationTokenSource = new CancellationTokenSource();
+            var secondCancellationTokenSource = new CancellationTokenSource();
+            var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationTokenSource.Token);
+            var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationTokenSource.Token);
+            var compositeChangeToken = new CompositeChangeToken(new[] { firstCancellationChangeToken, secondCancellationChangeToken });
+
+            var manualResetEvent = new ManualResetEvent(false);
+            compositeChangeToken.RegisterChangeCallback(_ => manualResetEvent.WaitOne(5000), null);
+
+            // Act & Assert
+            var firstChange = Task.Run(firstCancellationTokenSource.Cancel);
+            var secondChange = Task.Run(secondCancellationTokenSource.Cancel);
+            await Task.Delay(50);
+            manualResetEvent.Set();
+
+            await Task.WhenAll(firstChange, secondChange).WaitAsync(5000);
+        }
     }
 
     internal class ProxyCancellationChangeToken : IChangeToken
index 2e90ac8..00a7482 100644 (file)
@@ -14,4 +14,9 @@
     <ProjectReference Include="..\src\Microsoft.Extensions.Primitives.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs"
+             Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
+  </ItemGroup>
+
 </Project>