Skip to content

Commit

Permalink
Allow PeriodicTimer to have an infinite timeout (#82986)
Browse files Browse the repository at this point in the history
* Allow PeriodicTimer to have an infinite timeout

When PeriodicTimer was introduced, we prohibited infinite timeouts as they made no sense: you couldn't change the period after construction, so creating a PeriodicTimer with an infinite timeout meant creating an instance that would never tick.  But now that we've added a Period property with a setter that allows changing the period, infinite timeouts do make sense: as with Timer, they can be used as a mechanism to stop and start the timer, e.g. creating the PeriodicTimer with infinite and only in response to some later action updating its Period to start it.

* Update PeriodicTimer.cs
  • Loading branch information
stephentoub authored Mar 6, 2023
1 parent d39dbac commit 8ae17db
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace System.Threading
/// <remarks>
/// This timer is intended to be used only by a single consumer at a time: only one call to <see cref="WaitForNextTickAsync" />
/// may be in flight at any given moment. <see cref="Dispose"/> may be used concurrently with an active <see cref="WaitForNextTickAsync"/>
/// to interrupt it and cause it to return false.
/// to interrupt it and cause it to return false. Similarly, <see cref="Period"/> may be used concurrently with a consumer accessing
/// <see cref="WaitForNextTickAsync"/> in order to change the timer's period.
/// </remarks>
public sealed class PeriodicTimer : IDisposable
{
Expand All @@ -23,7 +24,7 @@ public sealed class PeriodicTimer : IDisposable

/// <summary>Initializes the timer.</summary>
/// <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>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>.</exception>
public PeriodicTimer(TimeSpan period)
{
if (!TryGetMilliseconds(period, out uint ms))
Expand All @@ -37,15 +38,15 @@ public PeriodicTimer(TimeSpan period)
}

/// <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>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or 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);
get => _timer._period == Timeout.UnsignedInfinite ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(_timer._period);
set
{
if (!TryGetMilliseconds(value, out uint ms))
Expand All @@ -65,7 +66,7 @@ public TimeSpan Period
private static bool TryGetMilliseconds(TimeSpan value, out uint milliseconds)
{
long ms = (long)value.TotalMilliseconds;
if (ms >= 1 && ms <= Timer.MaxSupportedTimeout)
if ((ms >= 1 && ms <= Timer.MaxSupportedTimeout) || value == Timeout.InfiniteTimeSpan)
{
milliseconds = (uint)ms;
return true;
Expand Down Expand Up @@ -121,6 +122,11 @@ private sealed class State : IValueTaskSource<bool>
/// PeriodicTimer to be finalized and unroot the TimerQueueTimer. Thus, we keep this field set during<see cref="WaitForNextTickAsync"/>
/// so that the timer roots any async continuation chain awaiting it, and then keep it unset otherwise so that everything
/// can be GC'd appropriately.
///
/// Note that if the period is set to infinite, even when there's an active waiter the PeriodicTimer won't
/// be rooted because TimerQueueTimer won't be rooted via the static linked list. That's fine, as the timer
/// will never tick in such a case, and for the timer's period to be changed, the user's code would need
/// some other reference to PeriodicTimer keeping it alive, anyway.
/// </remarks>
private PeriodicTimer? _owner;
/// <summary>Core of the <see cref="IValueTaskSource{TResult}"/> implementation.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public class PeriodicTimerTests
[Fact]
public void Ctor_InvalidArguments_Throws()
{
AssertExtensions.Throws<ArgumentOutOfRangeException>("period", () => new PeriodicTimer(TimeSpan.FromMilliseconds(-1)));
AssertExtensions.Throws<ArgumentOutOfRangeException>("period", () => new PeriodicTimer(TimeSpan.Zero));
AssertExtensions.Throws<ArgumentOutOfRangeException>("period", () => new PeriodicTimer(TimeSpan.FromMilliseconds(uint.MaxValue)));
}

[Theory]
[InlineData(-1)]
[InlineData(1)]
[InlineData(uint.MaxValue - 1)]
public void Ctor_ValidArguments_Succeeds(uint milliseconds)
public void Ctor_ValidArguments_Succeeds(double milliseconds)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(milliseconds));
}
Expand All @@ -29,7 +29,6 @@ public void Ctor_ValidArguments_Succeeds(uint milliseconds)
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));

Expand All @@ -43,6 +42,9 @@ public void Period_Roundtrips()
using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Period);

timer.Period = Timeout.InfiniteTimeSpan;
Assert.Equal(Timeout.InfiniteTimeSpan, timer.Period);

timer.Period = TimeSpan.FromDays(1);
Assert.Equal(TimeSpan.FromDays(1), timer.Period);

Expand All @@ -53,7 +55,7 @@ public void Period_Roundtrips()
[Fact]
public async Task Period_AffectsPendingWaits()
{
using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromDays(40));
using PeriodicTimer timer = new PeriodicTimer(Timeout.InfiniteTimeSpan);

ValueTask<bool> task = timer.WaitForNextTickAsync();
Assert.False(task.IsCompleted);
Expand Down

0 comments on commit 8ae17db

Please sign in to comment.