From 56d223b404113aea5132267ea28765a2da8b5603 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Thu, 20 Apr 2023 16:12:38 -0700 Subject: [PATCH 1/2] Adding TimeProvider CreateCancellationTokenSource extension method --- .../Common/tests/System/TimeProviderTests.cs | 42 ++++++++++--------- .../ref/Microsoft.Bcl.TimeProvider.Common.cs | 1 + .../Tasks/TimeProviderTaskExtensions.cs | 41 ++++++++++++++++++ 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs index 53e88feb97d2b7..472ea3ce18c630 100644 --- a/src/libraries/Common/tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -213,33 +213,34 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) // // Test out some int-based timeout logic // -#if NETFRAMEWORK - CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan); // should be an infinite timeout +#if TESTEXTENSIONS + CancellationTokenSource cts = provider.CreateCancellationTokenSource(Timeout.InfiniteTimeSpan); // should be an infinite timeout #else CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout #endif // NETFRAMEWORK - CancellationToken token = cts.Token; ManualResetEventSlim mres = new ManualResetEventSlim(false); - CancellationTokenRegistration ctr = token.Register(() => mres.Set()); - Assert.False(token.IsCancellationRequested, + Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on infinite timeout (int)!"); -#if NETFRAMEWORK - CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000000)); +#if TESTEXTENSIONS + cts.Dispose(); + cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1000000)); #else cts.CancelAfter(1000000); #endif // NETFRAMEWORK - Assert.False(token.IsCancellationRequested, + Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !"); -#if NETFRAMEWORK - CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1)); +#if TESTEXTENSIONS + cts.Dispose(); + cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); #else cts.CancelAfter(1); #endif // NETFRAMEWORK + CancellationTokenRegistration ctr = cts.Token.Register(() => mres.Set()); Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened"); mres.Wait(); @@ -250,33 +251,34 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) // Test out some TimeSpan-based timeout logic // TimeSpan prettyLong = new TimeSpan(1, 0, 0); -#if NETFRAMEWORK - cts = new CancellationTokenSource(prettyLong); +#if TESTEXTENSIONS + cts = provider.CreateCancellationTokenSource(prettyLong); #else cts = new CancellationTokenSource(prettyLong, provider); #endif // NETFRAMEWORK - token = cts.Token; mres = new ManualResetEventSlim(false); - ctr = token.Register(() => mres.Set()); - Assert.False(token.IsCancellationRequested, + Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,1)!"); -#if NETFRAMEWORK - CancelAfter(provider, cts, prettyLong); +#if TESTEXTENSIONS + cts.Dispose(); + cts = provider.CreateCancellationTokenSource(prettyLong); #else cts.CancelAfter(prettyLong); #endif // NETFRAMEWORK - Assert.False(token.IsCancellationRequested, + Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !"); -#if NETFRAMEWORK - CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000)); +#if TESTEXTENSIONS + cts.Dispose(); + cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1000)); #else cts.CancelAfter(new TimeSpan(1000)); #endif // NETFRAMEWORK + ctr = cts.Token.Register(() => mres.Set()); Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened"); diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs index 627405d04ac535..9c379eecfbaa95 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs @@ -8,5 +8,6 @@ public static class TimeProviderTaskExtensions public static System.Threading.Tasks.Task Delay(this System.TimeProvider timeProvider, System.TimeSpan delay, System.Threading.CancellationToken cancellationToken = default) { throw null; } public static System.Threading.Tasks.Task WaitAsync(this System.Threading.Tasks.Task task, System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken = default) { throw null; } public static System.Threading.Tasks.Task WaitAsync(this System.Threading.Tasks.Task task, System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public static System.Threading.CancellationTokenSource CreateCancellationTokenSource(this System.TimeProvider timeProvider, System.TimeSpan delay) { throw null; } } } diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs index 3a6fa1debbb670..aaff79e3958a86 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs @@ -218,5 +218,46 @@ public static async Task WaitAsync(this Task task, Ti return task.Result; } #endif // NET8_0_OR_GREATER + + /// Initializes a new instance of the class that will be canceled after the specified . + /// The with which to interpret the . + /// The time interval to wait before canceling this . + /// The is negative and not equal to or greater than maximum allowed timer duration. + /// that will be canceled after the specified . + /// + /// The countdown for the delay starts during the call to the constructor. When the delay expires, + /// the constructed is canceled if it has + /// not been canceled already. + /// If running on framework version prior to .NET 8.0, there is a constraint when invoking on the resultant object. + /// This action will not terminate the initial timer indicated by . However, this restriction does not apply on .NET 8.0 and later versions. + /// + public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay) + { +#if NET8_0_OR_GREATER + return new CancellationTokenSource(delay, timeProvider); +#else + if (timeProvider is null) + { + throw new ArgumentNullException(nameof(timeProvider)); + } + + if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(delay)); + } + + var cts = new CancellationTokenSource(); + + if (timeProvider == TimeProvider.System) + { + cts.CancelAfter(delay); + return cts; + } + + ITimer timer = timeProvider.CreateTimer(s => ((CancellationTokenSource)s).Cancel(), cts, delay, Timeout.InfiniteTimeSpan); + cts.Token.Register(t => ((ITimer)t).Dispose(), timer); + return cts; +#endif // NET8_0_OR_GREATER + } } } From ba5d1a6f657727e7603ee73cb110d18a1c3c1ec7 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Sun, 23 Apr 2023 11:14:53 -0700 Subject: [PATCH 2/2] Address the feedback --- .../Common/tests/System/TimeProviderTests.cs | 72 +++++++++++++++++-- .../Tasks/TimeProviderTaskExtensions.cs | 27 +++++-- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs index 472ea3ce18c630..51edf50d2ea37e 100644 --- a/src/libraries/Common/tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -217,7 +217,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) CancellationTokenSource cts = provider.CreateCancellationTokenSource(Timeout.InfiniteTimeSpan); // should be an infinite timeout #else CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS ManualResetEventSlim mres = new ManualResetEventSlim(false); Assert.False(cts.Token.IsCancellationRequested, @@ -228,7 +228,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1000000)); #else cts.CancelAfter(1000000); -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !"); @@ -238,7 +238,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); #else cts.CancelAfter(1); -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS CancellationTokenRegistration ctr = cts.Token.Register(() => mres.Set()); Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened"); @@ -255,7 +255,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) cts = provider.CreateCancellationTokenSource(prettyLong); #else cts = new CancellationTokenSource(prettyLong, provider); -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS mres = new ManualResetEventSlim(false); @@ -267,7 +267,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) cts = provider.CreateCancellationTokenSource(prettyLong); #else cts.CancelAfter(prettyLong); -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS Assert.False(cts.Token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !"); @@ -277,7 +277,7 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) cts = provider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1000)); #else cts.CancelAfter(new TimeSpan(1000)); -#endif // NETFRAMEWORK +#endif // TESTEXTENSIONS ctr = cts.Token.Register(() => mres.Set()); Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened"); @@ -557,5 +557,65 @@ public Task WaitAsync(Task task, TimeSpan timeout, Ti private static TestExtensionsTaskFactory extensionsTaskFactory = new(); #endif // TESTEXTENSIONS + + // A timer that get fired on demand + private class ManualTimer : ITimer + { + TimerCallback _callback; + object? _state; + + public ManualTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + _callback = callback; + _state = state; + } + + public bool Change(TimeSpan dueTime, TimeSpan period) => true; + + public void Fire() + { + _callback?.Invoke(_state); + IsFired = true; + } + + public bool IsFired { get; set; } + + public void Dispose() { } + public ValueTask DisposeAsync () { return default; } + } + + private class ManualTimeProvider : TimeProvider + { + public ManualTimer Timer { get; set; } + + public ManualTimeProvider() { } + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + Timer = new ManualTimer(callback, state, dueTime, period); + return Timer; + } + } + + [Fact] + // 1- Creates the CTS with a delay that we control via the time provider. + // 2- Disposes the CTS. + // 3- Then fires the timer. We want to validate the process doesn't crash. + public static void TestCTSWithDelayFiringAfterCancellation() + { + ManualTimeProvider manualTimer = new ManualTimeProvider(); +#if TESTEXTENSIONS + CancellationTokenSource cts = manualTimer.CreateCancellationTokenSource(TimeSpan.FromSeconds(60)); +#else + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(60), manualTimer); +#endif // TESTEXTENSIONS + + Assert.NotNull(manualTimer.Timer); + Assert.False(manualTimer.Timer.IsFired); + + cts.Dispose(); + + manualTimer.Timer.Fire(); + Assert.True(manualTimer.Timer.IsFired); + } } } diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs index aaff79e3958a86..c0e06f629d0384 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs @@ -6,6 +6,10 @@ namespace System.Threading.Tasks /// /// Provide extensions methods for operations with . /// + /// + /// The Microsoft.Bcl.TimeProvider library interfaces are intended solely for use in building against pre-.NET 8 surface area. + /// If your code is being built against .NET 8 or higher, then this library should not be utilized. + /// public static class TimeProviderTaskExtensions { #if !NET8_0_OR_GREATER @@ -225,11 +229,15 @@ public static async Task WaitAsync(this Task task, Ti /// The is negative and not equal to or greater than maximum allowed timer duration. /// that will be canceled after the specified . /// + /// /// The countdown for the delay starts during the call to the constructor. When the delay expires, /// the constructed is canceled if it has /// not been canceled already. - /// If running on framework version prior to .NET 8.0, there is a constraint when invoking on the resultant object. + /// + /// + /// If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking on the resultant object. /// This action will not terminate the initial timer indicated by . However, this restriction does not apply on .NET 8.0 and later versions. + /// /// public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay) { @@ -246,15 +254,22 @@ public static CancellationTokenSource CreateCancellationTokenSource(this TimePro throw new ArgumentOutOfRangeException(nameof(delay)); } - var cts = new CancellationTokenSource(); - if (timeProvider == TimeProvider.System) { - cts.CancelAfter(delay); - return cts; + return new CancellationTokenSource(delay); } - ITimer timer = timeProvider.CreateTimer(s => ((CancellationTokenSource)s).Cancel(), cts, delay, Timeout.InfiniteTimeSpan); + var cts = new CancellationTokenSource(); + + ITimer timer = timeProvider.CreateTimer(s => + { + try + { + ((CancellationTokenSource)s).Cancel(); + } + catch (ObjectDisposedException) { } + }, cts, delay, Timeout.InfiniteTimeSpan); + cts.Token.Register(t => ((ITimer)t).Dispose(), timer); return cts; #endif // NET8_0_OR_GREATER