Skip to content

Extend allowed Task.Delay/CTS TimeSpan values to match Timer #43708

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

Merged
merged 1 commit into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3359,7 +3359,7 @@
<value>The specified TaskContinuationOptions excluded all continuation kinds.</value>
</data>
<data name="Task_Delay_InvalidDelay" xml:space="preserve">
<value>The value needs to translate in milliseconds to -1 (signifying an infinite timeout), 0 or a positive integer less than or equal to Int32.MaxValue.</value>
<value>The value needs to translate in milliseconds to -1 (signifying an infinite timeout), 0 or a positive integer less than or equal to the maximum allowed timer duration.</value>
</data>
<data name="Task_Delay_InvalidMillisecondsDelay" xml:space="preserve">
<value>The value needs to be either -1 (signifying an infinite timeout), 0 or a positive integer.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ internal WaitHandle WaitHandle
/// </summary>
/// <param name="delay">The time span to wait before canceling this <see cref="CancellationTokenSource"/></param>
/// <exception cref="ArgumentOutOfRangeException">
/// The exception that is thrown when <paramref name="delay"/> is less than -1 or greater than int.MaxValue.
/// The <paramref name="delay"/> is less than -1 or greater than the maximum allowed timer duration.
/// </exception>
/// <remarks>
/// <para>
Expand All @@ -168,12 +168,12 @@ internal WaitHandle WaitHandle
public CancellationTokenSource(TimeSpan delay)
{
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(delay));
}

InitializeWithTimer((int)totalMilliseconds);
InitializeWithTimer((uint)totalMilliseconds);
}

/// <summary>
Expand Down Expand Up @@ -202,14 +202,14 @@ public CancellationTokenSource(int millisecondsDelay)
throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
}

InitializeWithTimer(millisecondsDelay);
InitializeWithTimer((uint)millisecondsDelay);
}

/// <summary>
/// Common initialization logic when constructing a CTS with a delay parameter.
/// A zero delay will result in immediate cancellation.
/// </summary>
private void InitializeWithTimer(int millisecondsDelay)
private void InitializeWithTimer(uint millisecondsDelay)
{
if (millisecondsDelay == 0)
{
Expand All @@ -218,7 +218,7 @@ private void InitializeWithTimer(int millisecondsDelay)
else
{
_state = NotCanceledState;
_timer = new TimerQueueTimer(s_timerCallback, this, (uint)millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
_timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);

// The timer roots this CTS instance while it's scheduled. That is by design, so
// that code like:
Expand Down Expand Up @@ -286,8 +286,7 @@ public void Cancel(bool throwOnFirstException)
/// cref="CancellationTokenSource"/> has been disposed.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// The exception thrown when <paramref name="delay"/> is less than -1 or
/// greater than int.MaxValue.
/// The <paramref name="delay"/> is less than -1 or greater than maximum allowed timer duration.
/// </exception>
/// <remarks>
/// <para>
Expand All @@ -303,12 +302,12 @@ public void Cancel(bool throwOnFirstException)
public void CancelAfter(TimeSpan delay)
{
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(delay));
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.delay);
}

CancelAfter((int)totalMilliseconds);
CancelAfter((uint)totalMilliseconds);
}

/// <summary>
Expand Down Expand Up @@ -337,13 +336,18 @@ public void CancelAfter(TimeSpan delay)
/// </remarks>
public void CancelAfter(int millisecondsDelay)
{
ThrowIfDisposed();

if (millisecondsDelay < -1)
{
throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay);
}

CancelAfter((uint)millisecondsDelay);
}

private void CancelAfter(uint millisecondsDelay)
{
ThrowIfDisposed();

if (IsCancellationRequested)
{
return;
Expand Down Expand Up @@ -379,7 +383,7 @@ public void CancelAfter(int millisecondsDelay)
// the following in a try/catch block.
try
{
timer.Change((uint)millisecondsDelay, Timeout.UnsignedInfinite);
timer.Change(millisecondsDelay, Timeout.UnsignedInfinite);
}
catch (ObjectDisposedException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5278,15 +5278,12 @@ public static Task<TResult> Run<TResult>(Func<Task<TResult>?> function, Cancella
/// <param name="delay">The time span to wait before completing the returned Task</param>
/// <returns>A Task that represents the time delay</returns>
/// <exception cref="System.ArgumentOutOfRangeException">
/// The <paramref name="delay"/> is less than -1 or greater than int.MaxValue.
/// The <paramref name="delay"/> is less than -1 or greater than the maximum allowed timer duration.
/// </exception>
/// <remarks>
/// After the specified time delay, the Task is completed in RanToCompletion state.
/// </remarks>
public static Task Delay(TimeSpan delay)
{
return Delay(delay, default);
}
public static Task Delay(TimeSpan delay) => Delay(delay, default);

/// <summary>
/// Creates a Task that will complete after a time delay.
Expand All @@ -5295,7 +5292,7 @@ public static Task Delay(TimeSpan delay)
/// <param name="cancellationToken">The cancellation token that will be checked prior to completing the returned Task</param>
/// <returns>A Task that represents the time delay</returns>
/// <exception cref="System.ArgumentOutOfRangeException">
/// The <paramref name="delay"/> is less than -1 or greater than int.MaxValue.
/// The <paramref name="delay"/> is less than -1 or greater than the maximum allowed timer duration.
/// </exception>
/// <exception cref="System.ObjectDisposedException">
/// The provided <paramref name="cancellationToken"/> has already been disposed.
Expand All @@ -5308,12 +5305,12 @@ public static Task Delay(TimeSpan delay)
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken)
{
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.delay, ExceptionResource.Task_Delay_InvalidDelay);
}

return Delay((int)totalMilliseconds, cancellationToken);
return Delay((uint)totalMilliseconds, cancellationToken);
}

/// <summary>
Expand All @@ -5327,10 +5324,7 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken)
/// <remarks>
/// After the specified time delay, the Task is completed in RanToCompletion state.
/// </remarks>
public static Task Delay(int millisecondsDelay)
{
return Delay(millisecondsDelay, default);
}
public static Task Delay(int millisecondsDelay) => Delay(millisecondsDelay, default);

/// <summary>
/// Creates a Task that will complete after a time delay.
Expand All @@ -5357,19 +5351,21 @@ public static Task Delay(int millisecondsDelay, CancellationToken cancellationTo
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay, ExceptionResource.Task_Delay_InvalidMillisecondsDelay);
}

return
cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) :
millisecondsDelay == 0 ? CompletedTask :
cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, cancellationToken) :
new DelayPromise(millisecondsDelay);
return Delay((uint)millisecondsDelay, cancellationToken);
}

private static Task Delay(uint millisecondsDelay, CancellationToken cancellationToken) =>
cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) :
millisecondsDelay == 0 ? CompletedTask :
cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, cancellationToken) :
new DelayPromise(millisecondsDelay);

/// <summary>Task that also stores the completion closure and logic for Task.Delay implementation.</summary>
private class DelayPromise : Task
{
private readonly TimerQueueTimer? _timer;

internal DelayPromise(int millisecondsDelay)
internal DelayPromise(uint millisecondsDelay)
{
Debug.Assert(millisecondsDelay != 0);

Expand All @@ -5379,9 +5375,9 @@ internal DelayPromise(int millisecondsDelay)
if (s_asyncDebuggingEnabled)
AddToActiveTasks(this);

if (millisecondsDelay != Timeout.Infinite) // no need to create the timer if it's an infinite timeout
if (millisecondsDelay != Timeout.UnsignedInfinite) // no need to create the timer if it's an infinite timeout
{
_timer = new TimerQueueTimer(state => ((DelayPromise)state!).CompleteTimedOut(), this, (uint)millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
_timer = new TimerQueueTimer(state => ((DelayPromise)state!).CompleteTimedOut(), this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
if (IsCompleted)
{
// Handle rare race condition where completion occurs prior to our having created and stored the timer, in which case
Expand Down Expand Up @@ -5414,7 +5410,7 @@ private sealed class DelayPromiseWithCancellation : DelayPromise
{
private readonly CancellationTokenRegistration _registration;

internal DelayPromiseWithCancellation(int millisecondsDelay, CancellationToken token) : base(millisecondsDelay)
internal DelayPromiseWithCancellation(uint millisecondsDelay, CancellationToken token) : base(millisecondsDelay)
{
Debug.Assert(token.CanBeCanceled);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ public ValueTask CloseAsync()

public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable
{
private const uint MAX_SUPPORTED_TIMEOUT = (uint)0xfffffffe;
internal const uint MaxSupportedTimeout = 0xfffffffe;

private TimerHolder _timer;

Expand Down Expand Up @@ -727,13 +727,13 @@ public Timer(TimerCallback callback,
long dueTm = (long)dueTime.TotalMilliseconds;
if (dueTm < -1)
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (dueTm > MAX_SUPPORTED_TIMEOUT)
if (dueTm > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_TimeoutTooLarge);

long periodTm = (long)period.TotalMilliseconds;
if (periodTm < -1)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (periodTm > MAX_SUPPORTED_TIMEOUT)
if (periodTm > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_PeriodTooLarge);

TimerSetup(callback, state, (uint)dueTm, (uint)periodTm);
Expand All @@ -757,9 +757,9 @@ public Timer(TimerCallback callback,
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (period < -1)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (dueTime > MAX_SUPPORTED_TIMEOUT)
if (dueTime > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_TimeoutTooLarge);
if (period > MAX_SUPPORTED_TIMEOUT)
if (period > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_PeriodTooLarge);
TimerSetup(callback, state, (uint)dueTime, (uint)period);
}
Expand Down Expand Up @@ -814,9 +814,9 @@ public bool Change(long dueTime, long period)
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (period < -1)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_NeedNonNegOrNegative1);
if (dueTime > MAX_SUPPORTED_TIMEOUT)
if (dueTime > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(dueTime), SR.ArgumentOutOfRange_TimeoutTooLarge);
if (period > MAX_SUPPORTED_TIMEOUT)
if (period > MaxSupportedTimeout)
throw new ArgumentOutOfRangeException(nameof(period), SR.ArgumentOutOfRange_PeriodTooLarge);

return _timer._timer.Change((uint)dueTime, (uint)period);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1052,27 +1052,28 @@ public static void CancellationTokenSourceWithTimer()
[Fact]
public static void CancellationTokenSourceWithTimer_Negative()
{
TimeSpan bigTimeSpan = new TimeSpan(2000, 0, 0, 0, 0);
TimeSpan bigTimeSpan = TimeSpan.FromMilliseconds(uint.MaxValue);
TimeSpan reasonableTimeSpan = new TimeSpan(0, 0, 1);
//
// Test exception logic
//
Assert.Throws<ArgumentOutOfRangeException>(
() => { new CancellationTokenSource(-2); });
Assert.Throws<ArgumentOutOfRangeException>(
() => { new CancellationTokenSource(bigTimeSpan); });

CancellationTokenSource cts = new CancellationTokenSource();
Assert.Throws<ArgumentOutOfRangeException>(
() => { cts.CancelAfter(-2); });
Assert.Throws<ArgumentOutOfRangeException>(
() => { cts.CancelAfter(bigTimeSpan); });
Assert.Throws<ArgumentOutOfRangeException>(() => { new CancellationTokenSource(-2); });
Assert.Throws<ArgumentOutOfRangeException>(() => { new CancellationTokenSource(bigTimeSpan); });

var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(uint.MaxValue - 1));
Assert.False(cts.IsCancellationRequested);
cts.Dispose();
Assert.Throws<ObjectDisposedException>(
() => { cts.CancelAfter(1); });
Assert.Throws<ObjectDisposedException>(
() => { cts.CancelAfter(reasonableTimeSpan); });

cts = new CancellationTokenSource();
Assert.Throws<ArgumentOutOfRangeException>(() => { cts.CancelAfter(-2); });
Assert.Throws<ArgumentOutOfRangeException>(() => { cts.CancelAfter(bigTimeSpan); });

cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(uint.MaxValue - 1));
Assert.False(cts.IsCancellationRequested);
cts.Dispose();

cts.Dispose();
Assert.Throws<ObjectDisposedException>(() => { cts.CancelAfter(1); });
Assert.Throws<ObjectDisposedException>(() => { cts.CancelAfter(reasonableTimeSpan); });
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
Expand Down
23 changes: 23 additions & 0 deletions src/libraries/System.Threading.Tasks/tests/Task/TaskRtTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,29 @@ public static void RunFromResult_FaultedTask()
Assert.True((bool)isHandledField.GetValue(holderObject), "Expected FromException task to be observed after accessing Exception");
}

[Theory]
[InlineData(-2L)]
[InlineData((long)int.MinValue)]
[InlineData((long)uint.MaxValue)]
public static void TaskDelay_OutOfBounds_ThrowsException(long delay)
{
AssertExtensions.Throws<ArgumentOutOfRangeException>("delay", () => { Task.Delay(TimeSpan.FromMilliseconds(delay)); });
if (delay >= int.MinValue && delay <= int.MaxValue)
{
AssertExtensions.Throws<ArgumentOutOfRangeException>("millisecondsDelay", () => { Task.Delay((int)delay); });
}
}

[Fact]
public static void TaskDelay_MaxSupported_Success()
{
var cts = new CancellationTokenSource();
Task t = Task.Delay(TimeSpan.FromMilliseconds(uint.MaxValue - 2), cts.Token);
Assert.False(t.IsCompleted);
cts.Cancel();
Assert.True(t.IsCanceled);
}

[Fact]
public static void RunDelayTests()
{
Expand Down