-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should TimeProviderTaskExtensions run continuations synchronously? #85326
Comments
Tagging subscribers to this area: @dotnet/area-system-datetime Issue DetailsFrom a test ergonomics point of view, having the backward-compatible I have a Line 18 in 277a28d
to public DelayState() : base() { } This allows me to write the following test: [Fact]
public void Callbacks_runs_synchronously()
{
// arrange
var sut = new ManualTimeProvider();
var callbackCount = 0;
_ = Continuation(sut, () => callbackCount++);
// act
sut.ForwardTime(TimeSpan.FromSeconds(10));
// assert
callbackCount.Should().Be(1);
static async Task Continuation(TimeProvider timeProvider, Action callback)
{
await timeProvider.Delay(TimeSpan.FromSeconds(10));
callback();
}
} With the original version that uses [Fact]
public async void Callbacks_runs_asynchronously()
{
// arrange
var sut = new ManualTimeProvider();
var callbackCount = 0;
_ = Continuation(sut, () => callbackCount++);
// act
sut.ForwardTime(TimeSpan.FromSeconds(10));
+ await Task.Yield();
// assert
callbackCount.Should().Be(1);
static async Task Continuation(TimeProvider timeProvider, Action callback)
{
await timeProvider.Delay(TimeSpan.FromSeconds(10));
callback();
}
} It is a small change, but it does make the test less readable, adds noise, and more importantly, makes the test non-deterministic. cc. @tarekgh.
|
CC @stephentoub |
I have continued to experiment with this, and I do think there is a difference here that needs to be addressed after trying out the preview 4 release of .NET 8 and the back-compatible package These two tests fail on // https://github.com/egil/TimeScheduler/blob/utilize-net8-lib/test/TimeProviderExtensions.Tests/ManualTimeProviderTests.cs#LL27C5-L73C6
[Fact]
public void Delay_callbacks_runs_synchronously()
{
// arrange
var sut = new ManualTimeProvider();
var callbackCount = 0;
_ = Continuation(sut, null);
// act
sut.ForwardTime(TimeSpan.FromSeconds(10));
// assert
callbackCount.Should().Be(1);
async Task Continuation(TimeProvider timeProvider, Action callback)
{
await timeProvider.Delay(TimeSpan.FromSeconds(10));
callback();
}
}
[Fact]
public void WaitAsync_callbacks_runs_synchronously()
{
// arrange
var sut = new ManualTimeProvider();
var callbackCount = 0;
_ = Continuation(sut, null);
// act
sut.ForwardTime(TimeSpan.FromSeconds(10));
// assert
callbackCount.Should().Be(1);
async Task Continuation(TimeProvider timeProvider, Action callback)
{
try
{
await Task.Delay(TimeSpan.FromDays(1)).WaitAsync(TimeSpan.FromSeconds(10), timeProvider);
}
catch (TimeoutException)
{
callback();
}
}
} The tests pass in both .NET 8 and .NET 6 if I add a I think they should behave the same, but I recognize that there may be things related to how I have implemented my |
Perhaps related: A similar issue is visible with The following test code will fail if [Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void PeriodicTimer_WaitForNextTickAsync_completes_iterations(int expectedCallbacks)
{
var sut = new ManualTimeProvider();
var calledTimes = 0;
var interval = TimeSpan.FromSeconds(1);
var looper = WaitForNextTickInLoop(sut, () => calledTimes++, interval);
sut.ForwardTime(interval * expectedCallbacks);
calledTimes.Should().Be(expectedCallbacks);
}
private static async Task WaitForNextTickInLoop(TimeProvider scheduler, Action callback, TimeSpan interval)
{
using var periodicTimer = scheduler.CreatePeriodicTimer(interval);
while (await periodicTimer.WaitForNextTickAsync(CancellationToken.None).ConfigureAwait(false))
{
callback();
}
} |
Thanks @egil for the info, this is very helpful. We'll look at this issue at some point. |
Suggestion: Make ManualTimeProvider.ForwardTime async (Task-returning) and have it yield. |
@pharring I tried adding a However, if I also change [Fact]
public async Task Delay_callbacks_runs_synchronously()
{
// arrange
var sut = new ManualTimeProvider();
var callbackCount = 0;
var continuationTask = Continuation(sut, () => callbackCount++);
// act
sut.ForwardTime(TimeSpan.FromSeconds(10));
// assert
callbackCount.Should().Be(1);
await continuationTask;
static async Task Continuation(TimeProvider timeProvider, Action callback)
{
await timeProvider.Delay(TimeSpan.FromSeconds(10));
+ // emulate stuff taking a long time
+ Thread.Sleep(100);
callback();
}
} So adding a sleep or yield in |
From a test ergonomics point of view, having the backward-compatible
TimeProvider
extensions methods run continuations asynchronously is bad for business.I have a
ManualTimeProvider
implementation I am currently using included in https://github.com/egil/TimeScheduler, where I ported theTimeProvider
type to .NET 6 as well as theTimeProviderTaskExtensions
type. However, in my implementation, I changed the following:runtime/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs
Line 18 in 277a28d
to
This allows me to write the following test:
With the original version that uses
TaskCreationOptions.RunContinuationsAsynchronously
, I have to add aTask.Yield()
or similar into the test after forwarding time, otherwise the test will fail.[Fact] public async void Callbacks_runs_asynchronously() { // arrange var sut = new ManualTimeProvider(); var callbackCount = 0; _ = Continuation(sut, () => callbackCount++); // act sut.ForwardTime(TimeSpan.FromSeconds(10)); + await Task.Yield(); // assert callbackCount.Should().Be(1); static async Task Continuation(TimeProvider timeProvider, Action callback) { await timeProvider.Delay(TimeSpan.FromSeconds(10)); callback(); } }
It is a small change, but it does make the test less readable, adds noise, and more importantly, makes the test non-deterministic.
cc. @tarekgh.
The text was updated successfully, but these errors were encountered: