Skip to content

Down-level TimeProviderTaskExtensions CancellationToken behavior is inconsistent with Task #106996

@brantburnett

Description

@brantburnett

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.

if (delay == TimeSpan.Zero)
{
return Task.CompletedTask;
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}

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.

if (timeout == TimeSpan.Zero)
{
return Task.FromException(new TimeoutException());
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}

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

Metadata

Metadata

Assignees

Labels

area-System.DateTimein-prThere is an active PR which will close this issue when it is mergedtenet-compatibilityIncompatibility with previous versions or .NET Framework

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions