Search code examples
c#.net-core

ChangeToken.OnChange causes stack overflow exception


I created this test using .NET 8 on Ubuntu 24.04:

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Shouldly;

[TestFixture]
public class PhysicalFileProviderOnChangeTests
{
  private string _tempDirectory;
  private string _testFilePath;

  [SetUp]
  public void SetUp()
  {
    _tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
    Directory.CreateDirectory(_tempDirectory);

    _testFilePath = Path.Combine(_tempDirectory, "testfile.txt");
    File.WriteAllText(_testFilePath, "Hello, World!");

    var provider = new PhysicalFileProvider(_tempDirectory);
  }


  [Test]
  public async Task TestOnChange_Handler()
  {
    var provider = new PhysicalFileProvider(_tempDirectory);
    var changeToken = provider.Watch("testfile.txt");

    var changeDetected = false;

    ChangeToken.OnChange(
      () => changeToken,
      () => changeDetected = true
    );

    File.WriteAllText(_testFilePath, "New Content");

    await Task.Delay(100);

    changeDetected.ShouldBeTrue();
  }

  [TearDown]
  public void TearDown()
  {
    if (Directory.Exists(_tempDirectory))
    {
      Directory.Delete(_tempDirectory, true);
    }
  }
}

When running this test using dotnet test I get this exception:

The active test run was aborted. Reason: Test host process crashed : Stack overflow.
Repeat 10915 times:
--------------------------------
   at System.Threading.CancellationTokenSource.Register(System.Delegate, System.Object, System.Threading.SynchronizationContext, Sys
tem.Threading.ExecutionContext)
   at System.Threading.CancellationToken.Register(System.Delegate, System.Object, Boolean, Boolean)
   at Microsoft.Extensions.Internal.ChangeCallbackRegistrar.UnsafeRegisterChangeCallback[[System.__Canon, System.Private.CoreLib, Ve
rsion=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Action`1<System.Object>, System.Object, System.Threading.Ca
ncellationToken, System.Action`1<System.__Canon>, System.__Canon)
   at Microsoft.Extensions.Primitives.CancellationChangeToken.RegisterChangeCallback(System.Action`1<System.Object>, System.Object)
   at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration`1[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0
, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].RegisterChangeTokenCallback(Microsoft.Extensions.Primitives.IChangeToken)
   at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration`1[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0
, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].OnChangeTokenFired()
   at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration`1+<>c[[System.__Canon, System.Private.CoreLib, Version=8.0
.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].<RegisterChangeTokenCallback>b__7_0(System.Object)
   at System.Threading.CancellationTokenSource.Invoke(System.Delegate, System.Object, System.Threading.CancellationTokenSource)
--------------------------------
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean)
   at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher+<>c.<.cctor>b__43_0(System.Object)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, Sy
stem.Threading.ContextCallback, System.Object)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()

Running the same test in another project, it works.

Commenting out this block, the test fails due to the true/false assertion but it doesn't crash:

ChangeToken.OnChange(
  () => changeToken,  
  () => changeDetected = true
);

Solution

  • There is an opened issue in dotNet Runtime (with an unmerged potential fix).

    With a potential workaround mentioned there:

    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).

    So might be try to follow this workaround with smth like:

    ChangeToken.OnChange(
      () => new PhysicalFileProvider(_tempDirectory).Watch("testfile.txt"),
      () => changeDetected = true
    );