Add PeriodicTimer.Period property (#82560)
authorStephen Toub <stoub@microsoft.com>
Thu, 2 Mar 2023 17:01:06 +0000 (12:01 -0500)
committerGitHub <noreply@github.com>
Thu, 2 Mar 2023 17:01:06 +0000 (12:01 -0500)
src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System/Threading/PeriodicTimerTests.cs

index 7de0daf..6df8877 100644 (file)
@@ -22,19 +22,57 @@ namespace System.Threading
         private readonly State _state;
 
         /// <summary>Initializes the timer.</summary>
-        /// <param name="period">The time interval between invocations of callback..</param>
+        /// <param name="period">The period between ticks</param>
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
         public PeriodicTimer(TimeSpan period)
         {
-            long ms = (long)period.TotalMilliseconds;
-            if (ms < 1 || ms > Timer.MaxSupportedTimeout)
+            if (!TryGetMilliseconds(period, out uint ms))
             {
                 GC.SuppressFinalize(this);
                 throw new ArgumentOutOfRangeException(nameof(period));
             }
 
             _state = new State();
-            _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, (uint)ms, (uint)ms, flowExecutionContext: false);
+            _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false);
+        }
+
+        /// <summary>Gets or sets the period between ticks.</summary>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
+        /// <remarks>
+        /// All prior ticks of the timer, including any that may be waiting to be consumed by <see cref="WaitForNextTickAsync"/>,
+        /// are unaffected by changes to <see cref="Period"/>. Setting <see cref="Period"/> affects only and all subsequent times
+        /// at which the timer will tick.
+        /// </remarks>
+        public TimeSpan Period
+        {
+            get => TimeSpan.FromMilliseconds(_timer._period);
+            set
+            {
+                if (!TryGetMilliseconds(value, out uint ms))
+                {
+                    throw new ArgumentOutOfRangeException(nameof(value));
+                }
+
+                _timer.Change(ms, ms);
+            }
+        }
+
+        /// <summary>Tries to extract the number of milliseconds from <paramref name="value"/>.</summary>
+        /// <returns>
+        /// true if the number of milliseconds is extracted and stored into <paramref name="milliseconds"/>;
+        /// false if the number of milliseconds would be out of range of a timer.
+        /// </returns>
+        private static bool TryGetMilliseconds(TimeSpan value, out uint milliseconds)
+        {
+            long ms = (long)value.TotalMilliseconds;
+            if (ms >= 1 && ms <= Timer.MaxSupportedTimeout)
+            {
+                milliseconds = (uint)ms;
+                return true;
+            }
+
+            milliseconds = 0;
+            return false;
         }
 
         /// <summary>Wait for the next tick of the timer, or for the timer to be stopped.</summary>
index 2f0d63c..2fe1072 100644 (file)
@@ -14485,6 +14485,7 @@ namespace System.Threading
         public PeriodicTimer(System.TimeSpan period) { }
         public void Dispose() { }
         ~PeriodicTimer() { }
+        public System.TimeSpan Period { get { throw null; } set { } }
         public System.Threading.Tasks.ValueTask<bool> WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
     }
     public static partial class Timeout
index 05cdd8e..f96b0b1 100644 (file)
@@ -26,6 +26,43 @@ namespace System.Threading.Tests
         }
 
         [Fact]
+        public void Period_InvalidArguments_Throws()
+        {
+            PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(-1));
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.Zero);
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(uint.MaxValue));
+
+            timer.Dispose();
+            Assert.Throws<ObjectDisposedException>(() => timer.Period = TimeSpan.FromMilliseconds(100));
+        }
+
+        [Fact]
+        public void Period_Roundtrips()
+        {
+            using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
+            Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Period);
+
+            timer.Period = TimeSpan.FromDays(1);
+            Assert.Equal(TimeSpan.FromDays(1), timer.Period);
+
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.Zero);
+            Assert.Equal(TimeSpan.FromDays(1), timer.Period);
+        }
+
+        [Fact]
+        public async void Period_AffectsPendingWaits()
+        {
+            using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromDays(40));
+
+            ValueTask<bool> task = timer.WaitForNextTickAsync();
+            Assert.False(task.IsCompleted);
+
+            timer.Period = TimeSpan.FromMilliseconds(1);
+            await task;
+        }
+
+        [Fact]
         public async Task Dispose_Idempotent()
         {
             var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));