From 5fa6e3f987d8b0b403c09000bfbb41d71aa7bc13 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Sun, 2 Apr 2023 17:41:05 -0700 Subject: [PATCH 1/5] Introducing Time abstraction Part2 (downlevel support) --- .../src/System/{ => Threading}/ITimer.cs | 0 .../Common/src/System/TimeProvider.cs | 75 +++++++- .../{Tests => }/System/TimeProviderTests.cs | 85 ++++++++- .../Microsoft.Bcl.TimeProvider.sln | 41 +++++ .../Microsoft.Bcl.TimeProvider.Forwards.cs | 5 + .../ref/Microsoft.Bcl.TimeProvider.cs | 38 ++++ .../ref/Microsoft.Bcl.TimeProvider.csproj | 16 ++ .../src/Microsoft.Bcl.TimeProvider.csproj | 34 ++++ .../src/Resources/Strings.resx | 69 +++++++ .../Tasks/TimeProviderTaskExtensions.cs | 171 ++++++++++++++++++ .../Microsoft.Bcl.TimeProvider.Tests.csproj | 13 ++ .../System.Private.CoreLib.Shared.projitems | 6 +- .../tests/System.Runtime.Tests.csproj | 4 +- 13 files changed, 542 insertions(+), 15 deletions(-) rename src/libraries/Common/src/System/{ => Threading}/ITimer.cs (100%) rename src/libraries/Common/tests/{Tests => }/System/TimeProviderTests.cs (86%) create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj diff --git a/src/libraries/Common/src/System/ITimer.cs b/src/libraries/Common/src/System/Threading/ITimer.cs similarity index 100% rename from src/libraries/Common/src/System/ITimer.cs rename to src/libraries/Common/src/System/Threading/ITimer.cs diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs index 316cde1dc3768..db64fe2d46d54 100644 --- a/src/libraries/Common/src/System/TimeProvider.cs +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -29,7 +29,15 @@ public abstract class TimeProvider /// Frequency of the values returned from method. protected TimeProvider(long timestampFrequency) { +#if SYSTEM_PRIVATE_CORELIB ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency); +#else + if (timestampFrequency <= 0) + { + throw new ArgumentOutOfRangeException(nameof(timestampFrequency), timestampFrequency, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero, nameof(timestampFrequency))); + } +#endif // SYSTEM_PRIVATE_CORELIB + TimestampFrequency = timestampFrequency; _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency; } @@ -53,9 +61,9 @@ public DateTimeOffset LocalNow TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime); long localTicks = utcDateTime.Ticks + offset.Ticks; - if ((ulong)localTicks > DateTime.MaxTicks) + if ((ulong)localTicks > (ulong)DateTime.MaxValue.Ticks) { - localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks; + localTicks = localTicks < DateTime.MinValue.Ticks ? DateTime.MinValue.Ticks : DateTime.MaxValue.Ticks; } return new DateTimeOffset(localTicks, offset); @@ -82,7 +90,15 @@ public DateTimeOffset LocalNow /// is null. public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone) { +#if SYSTEM_PRIVATE_CORELIB ArgumentNullException.ThrowIfNull(timeZone); +#else + if (timeZone is null) + { + throw new ArgumentNullException(nameof(timeZone)); + } +#endif // SYSTEM_PRIVATE_CORELIB + return new SystemTimeProvider(timeZone); } @@ -155,7 +171,15 @@ private sealed class SystemTimeProvider : TimeProvider /// public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { +#if SYSTEM_PRIVATE_CORELIB ArgumentNullException.ThrowIfNull(callback); +#else + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } +#endif // SYSTEM_PRIVATE_CORELIB + return new SystemTimeProviderTimer(dueTime, period, callback, state); } @@ -165,7 +189,7 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp /// public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - /// Thin wrapper for a . + /// Thin wrapper for a . /// /// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't /// want it exposed in a way that user code could directly queue the timer to the thread pool. @@ -174,12 +198,19 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp /// private sealed class SystemTimeProviderTimer : ITimer { +#if SYSTEM_PRIVATE_CORELIB private readonly TimerQueueTimer _timer; - +#else + private readonly Timer _timer; +#endif // SYSTEM_PRIVATE_CORELIB public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) { (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); +#if SYSTEM_PRIVATE_CORELIB _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true); +#else + _timer = new Timer(callback, state, duration, periodTime); +#endif // SYSTEM_PRIVATE_CORELIB } public bool Change(TimeSpan dueTime, TimeSpan period) @@ -190,17 +221,51 @@ public bool Change(TimeSpan dueTime, TimeSpan period) public void Dispose() => _timer.Dispose(); + +#if SYSTEM_PRIVATE_CORELIB public ValueTask DisposeAsync() => _timer.DisposeAsync(); +#else + public ValueTask DisposeAsync() + { + _timer.Dispose(); + return default; + } +#endif // SYSTEM_PRIVATE_CORELIB private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime) { long dueTm = (long)dueTime.TotalMilliseconds; + long periodTm = (long)periodTime.TotalMilliseconds; + +#if SYSTEM_PRIVATE_CORELIB ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime)); ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime)); - long periodTm = (long)periodTime.TotalMilliseconds; ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime)); ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime)); +#else + const uint MaxSupportedTimeout = 0xfffffffe; + + if (dueTm < -1) + { + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(dueTime), -1)); + } + + if (dueTm > MaxSupportedTimeout) + { + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(dueTime), MaxSupportedTimeout)); + } + + if (periodTm < -1) + { + throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(periodTm), -1)); + } + + if (periodTm > MaxSupportedTimeout) + { + throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(periodTm), MaxSupportedTimeout)); + } +#endif // SYSTEM_PRIVATE_CORELIB return ((uint)dueTm, (uint)periodTm); } diff --git a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs similarity index 86% rename from src/libraries/Common/tests/Tests/System/TimeProviderTests.cs rename to src/libraries/Common/tests/System/TimeProviderTests.cs index 8b00756367d88..3ea2f35a2129f 100644 --- a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -44,7 +44,11 @@ public void TestSystemProviderWithTimeZone() { Assert.Equal(TimeZoneInfo.Local.Id, TimeProvider.System.LocalTimeZone.Id); +#if NETFRAMEWORK + TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); +#else TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles"); +#endif // NETFRAMEWORK TimeProvider tp = TimeProvider.FromLocalTimeZone(tzi); Assert.Equal(tzi.Id, tp.LocalTimeZone.Id); @@ -57,6 +61,15 @@ public void TestSystemProviderWithTimeZone() Assert.InRange(utcConvertedDto.Ticks, utcDto1.Ticks, utcDto2.Ticks); } +#if NETFRAMEWORK + public static double s_tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency; + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + new TimeSpan((long)((endingTimestamp - startingTimestamp) * s_tickFrequency)); +#else + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); +#endif // NETFRAMEWORK + [Fact] public void TestSystemTimestamp() { @@ -68,7 +81,7 @@ public void TestSystemTimestamp() Assert.InRange(providerTimestamp1, timestamp1, timestamp2); Assert.True(providerTimestamp2 > timestamp2); - Assert.Equal(Stopwatch.GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2)); + Assert.Equal(GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2)); Assert.Equal(Stopwatch.Frequency, TimeProvider.System.TimestampFrequency); } @@ -151,6 +164,21 @@ public static IEnumerable TimersProvidersListData() yield return new object[] { new FastClock() }; } +#if NETFRAMEWORK + private static void CancelAfter(TimeProvider provider, CancellationTokenSource cts, TimeSpan delay) + { + if (provider == TimeProvider.System) + { + cts.CancelAfter(delay); + } + else + { + ITimer timer = provider.CreateTimer(s => ((CancellationTokenSource)s).Cancel(), cts, delay, Timeout.InfiniteTimeSpan); + cts.Token.Register(t => ((ITimer)t).Dispose(), timer); + } + } +#endif // NETFRAMEWORK + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [MemberData(nameof(TimersProvidersListData))] public static void CancellationTokenSourceWithTimer(TimeProvider provider) @@ -158,7 +186,11 @@ 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 +#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()); @@ -166,12 +198,20 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on infinite timeout (int)!"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000000)); +#else cts.CancelAfter(1000000); +#endif // NETFRAMEWORK Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1)); +#else cts.CancelAfter(1); +#endif // NETFRAMEWORK Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened"); @@ -183,7 +223,12 @@ 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); +#else cts = new CancellationTokenSource(prettyLong, provider); +#endif // NETFRAMEWORK + token = cts.Token; mres = new ManualResetEventSlim(false); ctr = token.Register(() => mres.Set()); @@ -191,12 +236,20 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,1)!"); +#if NETFRAMEWORK + CancelAfter(provider, cts, prettyLong); +#else cts.CancelAfter(prettyLong); +#endif // NETFRAMEWORK Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000)); +#else cts.CancelAfter(new TimeSpan(1000)); +#endif // NETFRAMEWORK Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened"); @@ -213,8 +266,13 @@ public static void RunDelayTests(TimeProvider provider) CancellationToken token = cts.Token; // These should all complete quickly, with RAN_TO_COMPLETION status. +#if NETFRAMEWORK + Task task1 = provider.Delay(new TimeSpan(0)); + Task task2 = provider.Delay(new TimeSpan(0), token); +#else Task task1 = Task.Delay(new TimeSpan(0), provider); Task task2 = Task.Delay(new TimeSpan(0), provider, token); +#endif // NETFRAMEWORK Debug.WriteLine("RunDelayTests: > Waiting for 0-delayed uncanceled tasks to complete. If we hang, something went wrong."); try @@ -230,7 +288,11 @@ public static void RunDelayTests(TimeProvider provider) Assert.True(task2.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion"); // This should take some time +#if NETFRAMEWORK + Task task3 = provider.Delay(TimeSpan.FromMilliseconds(20000)); +#else Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider); +#endif // NETFRAMEWORK Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(20000) appears to have completed too soon(1)."); Task t2 = Task.Delay(TimeSpan.FromMilliseconds(10)); Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(10000) appears to have completed too soon(2)."); @@ -242,16 +304,16 @@ public static async void RunWaitAsyncTests(TimeProvider provider) { CancellationTokenSource cts = new CancellationTokenSource(); - var tcs1 = new TaskCompletionSource(); + var tcs1 = new TaskCompletionSource(); Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider); Assert.False(task1.IsCompleted); - tcs1.SetResult(); + tcs1.SetResult(true); await task1; - var tcs2 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token); Assert.False(task2.IsCompleted); - tcs2.SetResult(); + tcs2.SetResult(true); await task2; var tcs3 = new TaskCompletionSource(); @@ -291,6 +353,7 @@ public static async void RunWaitAsyncTests(TimeProvider provider) Assert.Equal(200, await task8); } +#if !NETFRAMEWORK [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [MemberData(nameof(TimersProvidersListData))] public static async void PeriodicTimerTests(TimeProvider provider) @@ -304,6 +367,7 @@ public static async void PeriodicTimerTests(TimeProvider provider) timer.Dispose(); Assert.False(timer.WaitForNextTickAsync().Result); } +#endif // !NETFRAMEWORK [Fact] public static void NegativeTests() @@ -316,9 +380,11 @@ public static void NegativeTests() Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan)); Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2))); +#if !NETFRAMEWORK Assert.Throws(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null)); Assert.Throws(() => new PeriodicTimer(TimeSpan.FromMilliseconds(1), null)); +#endif // !NETFRAMEWORK } class TimerState @@ -414,7 +480,16 @@ public bool Change(TimeSpan dueTime, TimeSpan period) } public void Dispose() => _timer.Dispose(); + +#if NETFRAMEWORK + public ValueTask DisposeAsync() + { + _timer.Dispose(); + return default; + } +#else public ValueTask DisposeAsync() => _timer.DisposeAsync(); +#endif // NETFRAMEWORK } } } diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln b/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln new file mode 100644 index 0000000000000..e04f2b057d036 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln @@ -0,0 +1,41 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{7E9F6DE1-771B-4E25-A603-EC43D0291C8B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider", "ref\Microsoft.Bcl.TimeProvider.csproj", "{63655B2E-6A06-4E48-9F01-D0B910063165}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{98708A22-7268-4EDB-AE37-70AA958A772A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider", "src\Microsoft.Bcl.TimeProvider.csproj", "{B0716D7E-B824-4866-A1ED-DF31BA2970B9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8C3BD4AD-1A56-4204-9826-F8B74251D19F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider.Tests", "tests\Microsoft.Bcl.TimeProvider.Tests.csproj", "{E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.Build.0 = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.Build.0 = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {63655B2E-6A06-4E48-9F01-D0B910063165} = {7E9F6DE1-771B-4E25-A603-EC43D0291C8B} + {B0716D7E-B824-4866-A1ED-DF31BA2970B9} = {98708A22-7268-4EDB-AE37-70AA958A772A} + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191} = {8C3BD4AD-1A56-4204-9826-F8B74251D19F} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs new file mode 100644 index 0000000000000..cfdda781b4248 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.TimeProvider))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Threading.ITimer))] diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs new file mode 100644 index 0000000000000..4cbcbbba0ccb0 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ +namespace System +{ + public abstract class TimeProvider + { + public static TimeProvider System { get; } + protected TimeProvider(long timestampFrequency) { throw null; } + public abstract System.DateTimeOffset UtcNow { get; } + public System.DateTimeOffset LocalNow { get; } + public abstract System.TimeZoneInfo LocalTimeZone { get; } + public long TimestampFrequency { get; } + public static TimeProvider FromLocalTimeZone(System.TimeZoneInfo timeZone) { throw null; } + public abstract long GetTimestamp(); + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) { throw null; } + public abstract System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period); + } +} +namespace System.Threading +{ + public interface ITimer : System.IDisposable, System.IAsyncDisposable + { + bool Change(System.TimeSpan dueTime, System.TimeSpan period); + } +} + +namespace System.Threading.Tasks +{ + 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; } + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj new file mode 100644 index 0000000000000..0378e41e28168 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj @@ -0,0 +1,16 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj new file mode 100644 index 0000000000000..20d40e93b3a24 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj @@ -0,0 +1,34 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + true + + true + Provides support for system time abstraction primitives for .NET Framework and .NET Standard. + +Commonly Used Types: +System.TimeProvider +System.ITimer + + + + + + true + true + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx new file mode 100644 index 0000000000000..48056b82ad12a --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' must be a non-negative and non-zero value. + + + '{0}' must be greater than or equal to '{1}'. + + + '{0}' must be less than or equal to '{1}'. + + 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 new file mode 100644 index 0000000000000..b88b2b2560660 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks +{ + public static class TimeProviderTaskExtensions + { + private sealed class DelayState : TaskCompletionSource + { + public DelayState() : base(TaskCreationOptions.RunContinuationsAsynchronously) {} + public ITimer Timer { get; set; } + public CancellationTokenRegistration Registration { get; set; } + } + + public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) + { + if (timeProvider == TimeProvider.System) + { + return Task.Delay(delay, cancellationToken); + } + + if (timeProvider is null) + { + throw new ArgumentNullException(nameof(timeProvider)); + } + + if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(delay)); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (delay == TimeSpan.Zero) + { + return Task.CompletedTask; + } + + DelayState state = new(); + state.Timer = timeProvider.CreateTimer(delayState => + { + DelayState s = (DelayState)delayState; + s.TrySetResult(true); + s.Registration.Dispose(); + s.Timer.Dispose(); + }, state, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + + state.Timer.Change(delay, Timeout.InfiniteTimeSpan); + + state.Registration = cancellationToken.Register(delayState => + { + DelayState s = (DelayState)delayState; + s.TrySetCanceled(cancellationToken); + s.Timer.Dispose(); + s.Registration.Dispose(); + }, state); + + if (state.Task.IsCompleted) + { + state.Registration.Dispose(); + } + + return state.Task; + } + + private sealed class WaitAsyncState : TaskCompletionSource + { + public WaitAsyncState() : base(TaskCreationOptions.RunContinuationsAsynchronously) { } + public readonly CancellationTokenSource ContinuationCancellation = new CancellationTokenSource(); + public CancellationTokenRegistration Registration; + public ITimer? Timer; + } + + public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + { + if (task is null) + { + throw new ArgumentNullException(nameof(task)); + } + + if (timeout != Timeout.InfiniteTimeSpan && timeout < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + + if (timeProvider is null) + { + throw new ArgumentNullException(nameof(timeProvider)); + } + + if (task.IsCompleted) + { + return task; + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + var state = new WaitAsyncState(); + + state.Timer = timeProvider.CreateTimer(static s => + { + var state = (WaitAsyncState)s!; + + state.TrySetException(new TimeoutException()); + + state.Registration.Dispose(); + state.Timer!.Dispose(); + state.ContinuationCancellation.Cancel(); + }, state, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + state.Timer.Change(timeout, Timeout.InfiniteTimeSpan); + + _ = task.ContinueWith(static (t, s) => + { + var state = (WaitAsyncState)s!; + + if (t.IsFaulted) state.TrySetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) state.TrySetCanceled(); + else state.TrySetResult(true); + + state.Registration.Dispose(); + state.Timer?.Dispose(); + }, state, state.ContinuationCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + state.Registration = cancellationToken.Register(static s => + { + var state = (WaitAsyncState)s!; + + state.TrySetCanceled(); + + state.Timer?.Dispose(); + state.ContinuationCancellation.Cancel(); + }, state); + + if (state.Task.IsCompleted) + { + state.Registration.Dispose(); + } + + return state.Task; + } + + public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + WaitAsync((Task)task, timeout, timeProvider, cancellationToken).ConfigureAwait(false).GetAwaiter().OnCompleted(() => + { + if (task.IsCompleted) + { + tcs.TrySetResult(task.Result); + } + else if (cancellationToken.IsCancellationRequested) + { + tcs.TrySetCanceled(cancellationToken); + } + else + { + tcs.TrySetException(new TimeoutException()); + } + }); + + return tcs.Task; + } + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj new file mode 100644 index 0000000000000..2661a1e3dace9 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj @@ -0,0 +1,13 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + + + + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 79a84e6da1840..e216ff5b01529 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1322,9 +1322,6 @@ Common\SkipLocalsInit.cs - - Common\System\ITimer.cs - Common\System\TimeProvider.cs @@ -1385,6 +1382,9 @@ Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs + + Common\System\Threading\ITimer.cs + Common\System\Threading\OpenExistingResult.cs diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 266ac502fa531..82ff64b861d30 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -1,7 +1,7 @@  $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser - $(DefineConstants);TARGET_BROWSER + $(DefineConstants);TARGET_BROWSER true $(NoWarn),1718,SYSLIB0013 true @@ -41,8 +41,8 @@ + - From a5edcf3613d83128f8cb6f14e7d13b7384e343d1 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Mon, 3 Apr 2023 16:22:37 -0700 Subject: [PATCH 2/5] Address the feedback --- .../Common/src/System/TimeProvider.cs | 6 +- .../Common/tests/System/TimeProviderTests.cs | 96 ++++++++++++++----- .../ref/Microsoft.Bcl.TimeProvider.Common.cs | 12 +++ .../ref/Microsoft.Bcl.TimeProvider.cs | 10 -- .../ref/Microsoft.Bcl.TimeProvider.csproj | 6 +- .../src/Microsoft.Bcl.TimeProvider.csproj | 7 +- .../Tasks/TimeProviderTaskExtensions.cs | 81 ++++++++++------ .../Microsoft.Bcl.TimeProvider.Tests.csproj | 3 +- 8 files changed, 149 insertions(+), 72 deletions(-) create mode 100644 src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs index db64fe2d46d54..2ec90c076558d 100644 --- a/src/libraries/Common/src/System/TimeProvider.cs +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -209,7 +209,11 @@ public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback #if SYSTEM_PRIVATE_CORELIB _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true); #else - _timer = new Timer(callback, state, duration, periodTime); + // We want to ensure the timer we create will be tracked as long as it is scheduled. + // To do that, we call the constructor which track only the callback which will make the time to be tracked by the scheduler + // then we call Change on the timer to set the desired duration and period. + _timer = new Timer(_ => callback(state)); + _timer.Change(duration, periodTime); #endif // SYSTEM_PRIVATE_CORELIB } diff --git a/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs index 3ea2f35a2129f..2f169512adabd 100644 --- a/src/libraries/Common/tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -124,7 +124,7 @@ public void TestProviderTimer(TimeProvider provider, int MaxMilliseconds) state, TimeSpan.FromMilliseconds(state.Period), TimeSpan.FromMilliseconds(state.Period)); - state.TokenSource.Token.WaitHandle.WaitOne(30000); + state.TokenSource.Token.WaitHandle.WaitOne(60000); state.TokenSource.Dispose(); Assert.Equal(4, state.Counter); @@ -164,6 +164,17 @@ public static IEnumerable TimersProvidersListData() yield return new object[] { new FastClock() }; } + public static IEnumerable TimersProvidersWithTaskFactorData() + { + yield return new object[] { TimeProvider.System, taskFactory}; + yield return new object[] { new FastClock(), taskFactory }; + +#if TESTEXTENSIONS + yield return new object[] { TimeProvider.System, extensionsTaskFactory}; + yield return new object[] { new FastClock(), extensionsTaskFactory }; +#endif // TESTEXTENSIONS + } + #if NETFRAMEWORK private static void CancelAfter(TimeProvider provider, CancellationTokenSource cts, TimeSpan delay) { @@ -259,20 +270,15 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [MemberData(nameof(TimersProvidersListData))] - public static void RunDelayTests(TimeProvider provider) + [MemberData(nameof(TimersProvidersWithTaskFactorData))] + public static void RunDelayTests(TimeProvider provider, ITestTaskFactory taskFactory) { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // These should all complete quickly, with RAN_TO_COMPLETION status. -#if NETFRAMEWORK - Task task1 = provider.Delay(new TimeSpan(0)); - Task task2 = provider.Delay(new TimeSpan(0), token); -#else - Task task1 = Task.Delay(new TimeSpan(0), provider); - Task task2 = Task.Delay(new TimeSpan(0), provider, token); -#endif // NETFRAMEWORK + Task task1 = taskFactory.Delay(provider, new TimeSpan(0)); + Task task2 = taskFactory.Delay(provider, new TimeSpan(0), token); Debug.WriteLine("RunDelayTests: > Waiting for 0-delayed uncanceled tasks to complete. If we hang, something went wrong."); try @@ -288,67 +294,64 @@ public static void RunDelayTests(TimeProvider provider) Assert.True(task2.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion"); // This should take some time -#if NETFRAMEWORK - Task task3 = provider.Delay(TimeSpan.FromMilliseconds(20000)); -#else - Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider); -#endif // NETFRAMEWORK + Task task3 = taskFactory.Delay(provider, TimeSpan.FromMilliseconds(20000)); + Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(20000) appears to have completed too soon(1)."); Task t2 = Task.Delay(TimeSpan.FromMilliseconds(10)); Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(10000) appears to have completed too soon(2)."); } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [MemberData(nameof(TimersProvidersListData))] - public static async void RunWaitAsyncTests(TimeProvider provider) + [MemberData(nameof(TimersProvidersWithTaskFactorData))] + public static async void RunWaitAsyncTests(TimeProvider provider, ITestTaskFactory taskFactory) { CancellationTokenSource cts = new CancellationTokenSource(); var tcs1 = new TaskCompletionSource(); - Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider); + Task task1 = taskFactory.WaitAsync(tcs1.Task, TimeSpan.FromDays(1), provider); Assert.False(task1.IsCompleted); tcs1.SetResult(true); await task1; var tcs2 = new TaskCompletionSource(); - Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token); + Task task2 = taskFactory.WaitAsync(tcs2.Task, TimeSpan.FromDays(1), provider, cts.Token); Assert.False(task2.IsCompleted); tcs2.SetResult(true); await task2; var tcs3 = new TaskCompletionSource(); - Task task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider); + Task task3 = taskFactory.WaitAsync(tcs3.Task, TimeSpan.FromDays(1), provider); Assert.False(task3.IsCompleted); tcs3.SetResult(42); Assert.Equal(42, await task3); var tcs4 = new TaskCompletionSource(); - Task task4 = tcs4.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token); + Task task4 = taskFactory.WaitAsync(tcs4.Task, TimeSpan.FromDays(1), provider, cts.Token); Assert.False(task4.IsCompleted); tcs4.SetResult(42); Assert.Equal(42, await task4); using CancellationTokenSource cts1 = new CancellationTokenSource(); Task task5 = Task.Run(() => { while (!cts1.Token.IsCancellationRequested) { Thread.Sleep(10); } }); - await Assert.ThrowsAsync(() => task5.WaitAsync(TimeSpan.FromMilliseconds(10), provider)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task5, TimeSpan.FromMilliseconds(10), provider)); cts1.Cancel(); await task5; using CancellationTokenSource cts2 = new CancellationTokenSource(); Task task6 = Task.Run(() => { while (!cts2.Token.IsCancellationRequested) { Thread.Sleep(10); } }); - await Assert.ThrowsAsync(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task6, TimeSpan.FromMilliseconds(10), provider, cts2.Token)); cts1.Cancel(); await task5; using CancellationTokenSource cts3 = new CancellationTokenSource(); Task task7 = Task.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; }); - await Assert.ThrowsAsync(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task7, TimeSpan.FromMilliseconds(10), provider)); cts3.Cancel(); Assert.Equal(100, await task7); using CancellationTokenSource cts4 = new CancellationTokenSource(); Task task8 = Task.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; }); - await Assert.ThrowsAsync(() => task8.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts4.Token)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task8, TimeSpan.FromMilliseconds(10), provider, cts4.Token)); cts4.Cancel(); Assert.Equal(200, await task8); } @@ -491,5 +494,48 @@ public ValueTask DisposeAsync() public ValueTask DisposeAsync() => _timer.DisposeAsync(); #endif // NETFRAMEWORK } + + public interface ITestTaskFactory + { + Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default); + Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default); + Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default); + } + + private class TestTaskFactory : ITestTaskFactory + { + public Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default) + { +#if NETFRAMEWORK + return provider.Delay(delay, cancellationToken); +#else + return Task.Delay(delay, provider, cancellationToken); +#endif // NETFRAMEWORK + } + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, provider, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, provider, cancellationToken); + } + + private static TestTaskFactory taskFactory = new(); + +#if TESTEXTENSIONS + private class TestExtensionsTaskFactory : ITestTaskFactory + { + public Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.Delay(provider, delay, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.WaitAsync(task, timeout, provider, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.WaitAsync(task, timeout, provider, cancellationToken); + } + + private static TestExtensionsTaskFactory extensionsTaskFactory = new(); +#endif // TESTEXTENSIONS } } 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 new file mode 100644 index 0000000000000..627405d04ac53 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks +{ + 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; } + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs index 4cbcbbba0ccb0..3d11ef6fa5418 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs @@ -26,13 +26,3 @@ public interface ITimer : System.IDisposable, System.IAsyncDisposable bool Change(System.TimeSpan dueTime, System.TimeSpan period); } } - -namespace System.Threading.Tasks -{ - 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; } - } -} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj index 0378e41e28168..11d55537a3a3b 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj @@ -9,8 +9,10 @@ - + + + - + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj index 20d40e93b3a24..ecb1a244b6387 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj @@ -23,12 +23,15 @@ System.ITimer + + + - + - + 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 b88b2b2560660..f44193f6c1570 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 @@ -5,6 +5,7 @@ namespace System.Threading.Tasks { public static class TimeProviderTaskExtensions { +#if !NET8_0_OR_GREATER private sealed class DelayState : TaskCompletionSource { public DelayState() : base(TaskCreationOptions.RunContinuationsAsynchronously) {} @@ -12,8 +13,20 @@ private sealed class DelayState : TaskCompletionSource public CancellationTokenRegistration Registration { get; set; } } + private sealed class WaitAsyncState : TaskCompletionSource + { + public WaitAsyncState() : base(TaskCreationOptions.RunContinuationsAsynchronously) { } + public readonly CancellationTokenSource ContinuationCancellation = new CancellationTokenSource(); + public CancellationTokenRegistration Registration; + public ITimer? Timer; + } +#endif // !NET8_0_OR_GREATER + public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) { +#if NET8_0_OR_GREATER + return Task.Delay(delay, timeProvider, cancellationToken); +#else if (timeProvider == TimeProvider.System) { return Task.Delay(delay, cancellationToken); @@ -29,17 +42,20 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell throw new ArgumentOutOfRangeException(nameof(delay)); } - if (cancellationToken.IsCancellationRequested) + if (delay == TimeSpan.Zero) { - return Task.FromCanceled(cancellationToken); + return Task.CompletedTask; } - if (delay == TimeSpan.Zero) + if (cancellationToken.IsCancellationRequested) { - return Task.CompletedTask; + return Task.FromCanceled(cancellationToken); } DelayState state = new(); + + // To prevent a race condition where the timer may fire before being assigned to s.Timer, + // we initialize it with an InfiniteTimeSpan and then set it to the state variable, followed by calling Time.Change. state.Timer = timeProvider.CreateTimer(delayState => { DelayState s = (DelayState)delayState; @@ -58,24 +74,23 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell s.Registration.Dispose(); }, state); + // To prevent a race condition where the timer fires after we have attached the cancellation callback + // but before the registration is stored in state.Registration, we perform a subsequent check to ensure + // that the registration is not left dangling. if (state.Task.IsCompleted) { state.Registration.Dispose(); } return state.Task; - } - - private sealed class WaitAsyncState : TaskCompletionSource - { - public WaitAsyncState() : base(TaskCreationOptions.RunContinuationsAsynchronously) { } - public readonly CancellationTokenSource ContinuationCancellation = new CancellationTokenSource(); - public CancellationTokenRegistration Registration; - public ITimer? Timer; +#endif // NET8_0_OR_GREATER } public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) { +#if NET8_0_OR_GREATER + return task.WaitAsync(timeout, timeProvider, cancellationToken); +#else if (task is null) { throw new ArgumentNullException(nameof(task)); @@ -96,6 +111,16 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time return task; } + if (timeout == Timeout.InfiniteTimeSpan && !cancellationToken.CanBeCanceled) + { + return task; + } + + if (timeout == TimeSpan.Zero) + { + Task.FromException(new TimeoutException()); + } + if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); @@ -103,6 +128,8 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time var state = new WaitAsyncState(); + // To prevent a race condition where the timer may fire before being assigned to s.Timer, + // we initialize it with an InfiniteTimeSpan and then set it to the state variable, followed by calling Time.Change. state.Timer = timeProvider.CreateTimer(static s => { var state = (WaitAsyncState)s!; @@ -137,35 +164,27 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time state.ContinuationCancellation.Cancel(); }, state); + // To prevent a race condition where the timer fires after we have attached the cancellation callback + // but before the registration is stored in state.Registration, we perform a subsequent check to ensure + // that the registration is not left dangling. if (state.Task.IsCompleted) { state.Registration.Dispose(); } return state.Task; +#endif // NET8_0_OR_GREATER } +#if NET8_0_OR_GREATER public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, timeProvider, cancellationToken); +#else + public static async Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); - - WaitAsync((Task)task, timeout, timeProvider, cancellationToken).ConfigureAwait(false).GetAwaiter().OnCompleted(() => - { - if (task.IsCompleted) - { - tcs.TrySetResult(task.Result); - } - else if (cancellationToken.IsCancellationRequested) - { - tcs.TrySetCanceled(cancellationToken); - } - else - { - tcs.TrySetException(new TimeoutException()); - } - }); - - return tcs.Task; + await ((Task)task).WaitAsync(timeout, timeProvider, cancellationToken).ConfigureAwait(false); + return task.Result; } +#endif // NET8_0_OR_GREATER } } diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj index 2661a1e3dace9..f7e1b14dc4a66 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj @@ -1,9 +1,10 @@ $(NetCoreAppCurrent);$(NetFrameworkMinimum) + $(DefineConstants);TESTEXTENSIONS - + From 0917426e0fe0316355b6f5af7d96e6673d74ef19 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Tue, 4 Apr 2023 18:51:58 -0700 Subject: [PATCH 3/5] More feedback --- .../Common/src/System/TimeProvider.cs | 7 ++-- .../src/Microsoft.Bcl.TimeProvider.csproj | 1 + .../Tasks/TimeProviderTaskExtensions.cs | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs index 2ec90c076558d..2effc864d8073 100644 --- a/src/libraries/Common/src/System/TimeProvider.cs +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -49,6 +49,9 @@ protected TimeProvider(long timestampFrequency) /// public abstract DateTimeOffset UtcNow { get; } + private static readonly long s_minDateTicks = DateTime.MinValue.Ticks; + private static readonly long s_maxDateTicks = DateTime.MaxValue.Ticks; + /// /// Gets a value that is set to the current date and time according to this 's /// notion of time based on , with the offset set to the 's offset from Coordinated Universal Time (UTC). @@ -61,9 +64,9 @@ public DateTimeOffset LocalNow TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime); long localTicks = utcDateTime.Ticks + offset.Ticks; - if ((ulong)localTicks > (ulong)DateTime.MaxValue.Ticks) + if ((ulong)localTicks > (ulong)s_maxDateTicks) { - localTicks = localTicks < DateTime.MinValue.Ticks ? DateTime.MinValue.Ticks : DateTime.MaxValue.Ticks; + localTicks = localTicks < s_minDateTicks ? s_minDateTicks : s_maxDateTicks; } return new DateTimeOffset(localTicks, offset); diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj index ecb1a244b6387..4ecd0ecb26f80 100644 --- a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj @@ -6,6 +6,7 @@ Once this package has shipped a stable version, the following line should be removed in order to re-enable validation. --> true + Provides support for system time abstraction primitives for .NET Framework and .NET Standard. Commonly Used Types: 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 f44193f6c1570..cf96de6ead763 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 @@ -3,6 +3,9 @@ namespace System.Threading.Tasks { + /// + /// Provide extensions methods for operations with . + /// public static class TimeProviderTaskExtensions { #if !NET8_0_OR_GREATER @@ -22,6 +25,13 @@ private sealed class WaitAsyncState : TaskCompletionSource } #endif // !NET8_0_OR_GREATER + /// Creates a task that completes after a specified time interval. + /// The with which to interpret . + /// The to wait before completing the returned task, or to wait indefinitely. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the time delay. + /// The argument is null. + /// represents a negative time interval other than . public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) { #if NET8_0_OR_GREATER @@ -86,6 +96,17 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell #endif // NET8_0_OR_GREATER } + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The task needs to wait on until completion. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + /// The argument is null. + /// represents a negative time interval other than . public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) { #if NET8_0_OR_GREATER @@ -176,6 +197,17 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time #endif // NET8_0_OR_GREATER } + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The task needs to wait on until completion. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + /// The argument is null. + /// represents a negative time interval other than . #if NET8_0_OR_GREATER public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) => task.WaitAsync(timeout, timeProvider, cancellationToken); From f1e83340a542f33d30bfb3d094662c52967ade11 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Wed, 5 Apr 2023 11:15:37 -0700 Subject: [PATCH 4/5] Doc fix --- .../System/Threading/Tasks/TimeProviderTaskExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 cf96de6ead763..117843a1f37ba 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 @@ -31,7 +31,7 @@ private sealed class WaitAsyncState : TaskCompletionSource /// A cancellation token to observe while waiting for the task to complete. /// A task that represents the time delay. /// The argument is null. - /// represents a negative time interval other than . + /// represents a negative time interval other than . public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) { #if NET8_0_OR_GREATER @@ -106,7 +106,7 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. /// The argument is null. /// The argument is null. - /// represents a negative time interval other than . + /// represents a negative time interval other than . public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) { #if NET8_0_OR_GREATER @@ -207,7 +207,7 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. /// The argument is null. /// The argument is null. - /// represents a negative time interval other than . + /// represents a negative time interval other than . #if NET8_0_OR_GREATER public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) => task.WaitAsync(timeout, timeProvider, cancellationToken); From 21fd3317fef5f9eaf32ffbe7b06c3135679a962d Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Thu, 6 Apr 2023 15:30:13 -0700 Subject: [PATCH 5/5] More feedback addressing --- src/libraries/Common/src/System/TimeProvider.cs | 9 ++++++++- .../System/Threading/Tasks/TimeProviderTaskExtensions.cs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs index 2effc864d8073..b8d7fb9fa6aa2 100644 --- a/src/libraries/Common/src/System/TimeProvider.cs +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -223,7 +223,14 @@ public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback public bool Change(TimeSpan dueTime, TimeSpan period) { (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); - return _timer.Change(duration, periodTime); + try + { + return _timer.Change(duration, periodTime); + } + catch (ObjectDisposedException) + { + return false; + } } public void Dispose() => _timer.Dispose(); 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 117843a1f37ba..3a6fa1debbb67 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 @@ -99,7 +99,7 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell /// /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. /// - /// The task needs to wait on until completion. + /// The task for which to wait on until completion. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The with which to interpret . /// The to monitor for a cancellation request. @@ -200,7 +200,7 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time /// /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. /// - /// The task needs to wait on until completion. + /// The task for which to wait on until completion. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The with which to interpret . /// The to monitor for a cancellation request.