Skip to content

Cannot create a CancellationTokenSource with an infinite delay with TimeProvider that is not TimeProvider.System #88000

Description

@martincostello

Description

If code creates a CancellationTokenSource with an infinite delay, when that code is invoked with any TimeProvider instead of TimeProvider.System, an ArgumentOutOfRangeException is thrown.

I'm guessing this is caused by the code here that casts the long to a uint wrapping around, which takes the value 1 beyond the maximum value in the validation code for Timer.

public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.delay);
}
InitializeWithTimer((uint)totalMilliseconds, timeProvider);
}

internal const uint MaxSupportedTimeout = 0xfffffffe;

Reproduction Steps

Execute the following xunit test targeting net8.0:

using Microsoft.Extensions.Time.Testing;

namespace CancellationTokenSourceWithTimeProviderRepro;

public static class CancellationTokenSourceTests
{
    public static IEnumerable<object[]> TimeProviders => new[]
    {
        new[] { TimeProvider.System },
        new[] { new DerivedTimeProvider() },
        new[] { new FakeTimeProvider() },
    };

    [Theory]
    [MemberData(nameof(TimeProviders))]
    public static void Can_Create_Infinite_CancellationTokenSource_With_TimeProvider(TimeProvider timeProvider)
    {
        using var cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, timeProvider);
        Assert.False(cts.IsCancellationRequested);
    }

    private sealed class DerivedTimeProvider : TimeProvider
    {
        // Changes no behaviour, but is not the same underlying type/object as TimeProvider.System
    }
}

Expected behavior

All three tests pass.

Actual behavior

The tests that do not use TimeProvider.System both fail with similar exceptions:

Can Create Infinite CancellationTokenSource With TimeProvider(timeProvider: DerivedTimeProvider { LocalTimeZone = (UTC+00:00) Dublin, Edinburgh, Lisbon, London, TimestampFrequency = 10000000 }) 
Duration: 1 ms

Message: 
  System.ArgumentOutOfRangeException : dueTime ('4294967295') must be less than or equal to '4294967294'. (Parameter 'dueTime')
  Actual value was 4294967295.

Stack Trace: 
  ArgumentOutOfRangeException.ThrowGreater[T](String paramName, T value, T other)
  SystemTimeProviderTimer.CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
  SystemTimeProviderTimer.ctor(TimeSpan dueTime, TimeSpan period, TimerCallback callback, Object state)
  TimeProvider.CreateTimer(TimerCallback callback, Object state, TimeSpan dueTime, TimeSpan period)
  CancellationTokenSource.InitializeWithTimer(UInt32 millisecondsDelay, TimeProvider timeProvider)
  CancellationTokenSource.ctor(TimeSpan delay, TimeProvider timeProvider)
  CancellationTokenSourceTests.Can_Create_Infinite_CancellationTokenSource_With_TimeProvider(TimeProvider timeProvider) line 21
  InvokeStub_CancellationTokenSourceTests.Can_Create_Infinite_CancellationTokenSource_With_TimeProvider(Object, Object, IntPtr*)
  MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
Can Create Infinite CancellationTokenSource With TimeProvider(timeProvider: 2000-01-01T00:00:00.000) 
Duration: 1 ms

Message: 
  System.ArgumentOutOfRangeException : Argument not in the range [-1..4294967294] (Parameter 'dueTime')
  Actual value was 4294967295.

Stack Trace: 
  Throw.ArgumentOutOfRangeException(String paramName, Object actualValue, String message)
  Waiter.ChangeAndValidateDurations(TimeSpan dueTime, TimeSpan period)
  Waiter.ctor(FakeTimeProvider fakeTimeProvider, TimeSpan dueTime, TimeSpan period, TimerCallback callback, Object state)
  FakeTimeProviderTimer.ctor(FakeTimeProvider fakeTimeProvider, TimeSpan dueTime, TimeSpan period, TimerCallback callback, Object state)
  FakeTimeProvider.CreateTimer(TimerCallback callback, Object state, TimeSpan dueTime, TimeSpan period)
  CancellationTokenSource.InitializeWithTimer(UInt32 millisecondsDelay, TimeProvider timeProvider)
  CancellationTokenSource.ctor(TimeSpan delay, TimeProvider timeProvider)
  CancellationTokenSourceTests.Can_Create_Infinite_CancellationTokenSource_With_TimeProvider(TimeProvider timeProvider) line 21
  InvokeStub_CancellationTokenSourceTests.Can_Create_Infinite_CancellationTokenSource_With_TimeProvider(Object, Object, IntPtr*)
  MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

Regression?

Unknown.

Known Workarounds

None.

Configuration

  • .NET SDK 8.0.100-preview.5.23303.2
  • Microsoft.Extensions.TimeProvider.Testing 8.0.0-preview.5.23308.3

Other information

No response

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions