-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Description
The static ChangeToken.OnChange method throws a StackOverflowException when its producer factory returns the same change token instance.
Reproduction Steps
You will need a project with the Microsoft.Extensions.Primitives NuGet package installed. The following C# code demonstrates the bug:
using System;
using System.Threading;
using Microsoft.Extensions.Primitives;
CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);
Console.WriteLine($"Initial state of HasChanged: {cancellationChangeToken.HasChanged}");
Func<IChangeToken> producer = () =>
{
return cancellationChangeToken;
};
Action consumer = () => Console.WriteLine("The callback was invoked.");
using (ChangeToken.OnChange(producer, consumer))
{
cancellationTokenSource.Cancel();
}Expected behavior
I would expect that this method would not throw a StackOverflowException.
Actual behavior
A StackOverflowException is thrown. The ChangeToken.OnChange function registers the producer and corresponding consumer. When the producer triggers a change, i.e.; the cancellationTokenSource.Cancel is called, the consumer callback is executed and then the producer is called again to get a new change token. However, the same change token is returned. And since it has already fired, it immediately causes the re-registration to invoke the callback inline. This creates an infinite loop, where the symptom is the SOE.
Regression?
It is my understanding that this has always been an issue, it's just never been raised before. This ChangeToken is applicable to the following .NET builds:
| .NET Platform Extensions | 1.0, 1.1, 2.0, 2.1, 2.2, 3.0, 3.1, 5.0, 6.0 RC 1 |
|---|
Known Workarounds
The known workaround is to ensure that the producer either returns a new change token, or at least evaluates the previous state and resets itself (if possible).
using System;
using System.Threading;
using Microsoft.Extensions.Primitives;
CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);
Console.WriteLine($"Initial state of HasChanged: {cancellationChangeToken.HasChanged}");
Func<IChangeToken> producer = () =>
{
// The producer factory should always return a new change token.
// If the token's already fired, get a new token.
if (cancellationTokenSource.IsCancellationRequested)
{
cancellationTokenSource = new();
cancellationChangeToken = new(cancellationTokenSource.Token);
}
return cancellationChangeToken;
};
Action consumer = () => Console.WriteLine("The callback was invoked.");
using (ChangeToken.OnChange(producer, consumer))
{
cancellationTokenSource.Cancel();
}Configuration
No response
Other information
No response