-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Description
When using TimeProviderTaskExtensions from down-level runtimes, the Delay method which accepts a CancellationToken behaves inconsistently with the equivalent Task.Delay overload. Task.Delay checks CancellationToken.IsCancellationRequested first, whereas TimeProviderTaskExtensions.Delay checks the timeout for TimeSpan.Zero first. This means if the method is passed a zero timeout and an already canceled CancellationToken, Task.Delay returns Task.FromCanceled whereas TimeProviderTaskExtensions.Delay returns Task.CompletedTask.
Lines 69 to 77 in fb8e078
| if (delay == TimeSpan.Zero) | |
| { | |
| return Task.CompletedTask; | |
| } | |
| if (cancellationToken.IsCancellationRequested) | |
| { | |
| return Task.FromCanceled(cancellationToken); | |
| } |
runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
Lines 5725 to 5729 in 9a31a5b
| private static Task Delay(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken cancellationToken) => | |
| cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) : | |
| millisecondsDelay == 0 ? CompletedTask : | |
| cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, timeProvider, cancellationToken) : | |
| new DelayPromise(millisecondsDelay, timeProvider); |
The same problem also affects TimeProviderTaskExtensions.WaitAsync.
Lines 164 to 172 in 9a31a5b
| if (timeout == TimeSpan.Zero) | |
| { | |
| return Task.FromException(new TimeoutException()); | |
| } | |
| if (cancellationToken.IsCancellationRequested) | |
| { | |
| return Task.FromCanceled(cancellationToken); | |
| } |
runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
Lines 2837 to 2845 in 9a31a5b
| if (cancellationToken.IsCancellationRequested) | |
| { | |
| return FromCanceled(cancellationToken); | |
| } | |
| if (millisecondsTimeout == 0) | |
| { | |
| return FromException(new TimeoutException()); | |
| } |
Reproduction Steps
Target .NET < 8.0 (such as 4.6.2) and use a non-system time provider.
using var cts = new CancellationTokenSource();
cts.Cancel();
await new FakeTimeProvider().Delay(TimeSpan.Zero, cts.Token);and
var tcs = new TaskCompletionSource<object>();
using var cts = new CancellationTokenSource();
cts.Cancel();
await tcs.WaitAsync(TimeSpan.Zero, new FakeTimeProvider(), cts.Token);Expected behavior
The examples above should throw OperationCanceledException o for consistency, otherwise library authors converting their code to use TimeProvider may encounter unexpected behaviors.
Actual behavior
The Delay example above completes without an exception, the WaitAsync example throws TimeoutException.
Regression?
This is not a regression
Known Workarounds
Call CancellationToken.ThrowIfCancellationRequested() manually before calling Delay or WaitAsync. However, there is still a slim chance of a race condition in this case.
Configuration
No response
Other information
No response