-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: allow ManualTimer finalizer to run
- Loading branch information
Showing
4 changed files
with
178 additions
and
158 deletions.
There are no files selected for viewing
79 changes: 79 additions & 0 deletions
79
src/TimeProviderExtensions/ManualTimeProvider.ManualTimer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace TimeProviderExtensions; | ||
|
||
/// <summary> | ||
/// Represents a synthetic time provider that can be used to enable deterministic behavior in tests. | ||
/// </summary> | ||
/// <remarks> | ||
/// Learn more at <see href="https://github.com/egil/TimeProviderExtensions"/>. | ||
/// </remarks> | ||
public partial class ManualTimeProvider : TimeProvider | ||
{ | ||
private sealed class ManualTimer : ITimer | ||
{ | ||
private ManualTimerScheduler? scheduledCallback; | ||
|
||
public ManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider) | ||
{ | ||
scheduledCallback = new ManualTimerScheduler(timeProvider, callback, state); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public bool Change(TimeSpan dueTime, TimeSpan period) | ||
{ | ||
ValidateTimeSpanRange(dueTime); | ||
ValidateTimeSpanRange(period); | ||
|
||
if (scheduledCallback is null) | ||
{ | ||
return false; | ||
} | ||
|
||
scheduledCallback.Change(dueTime, period); | ||
|
||
return true; | ||
} | ||
|
||
// In case the timer is not disposed explicitly by the user. | ||
~ManualTimer() => Dispose(false); | ||
|
||
public void Dispose() | ||
{ | ||
Dispose(true); | ||
GC.SuppressFinalize(this); | ||
} | ||
|
||
public ValueTask DisposeAsync() | ||
{ | ||
Dispose(true); | ||
GC.SuppressFinalize(this); | ||
return ValueTask.CompletedTask; | ||
} | ||
|
||
private void Dispose(bool _) | ||
{ | ||
if (scheduledCallback is null) | ||
{ | ||
return; | ||
} | ||
|
||
scheduledCallback.Cancel(); | ||
scheduledCallback = null; | ||
} | ||
|
||
private static void ValidateTimeSpanRange(TimeSpan time, [CallerArgumentExpression("time")] string? parameter = null) | ||
{ | ||
long tm = (long)time.TotalMilliseconds; | ||
if (tm < -1) | ||
{ | ||
throw new ArgumentOutOfRangeException(parameter, $"{parameter}.TotalMilliseconds must be greater than -1."); | ||
} | ||
|
||
if (tm > MaxSupportedTimeout) | ||
{ | ||
throw new ArgumentOutOfRangeException(parameter, $"{parameter}.TotalMilliseconds must be less than than {MaxSupportedTimeout}."); | ||
} | ||
} | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
src/TimeProviderExtensions/ManualTimeProvider.ManualTimerScheduledCallback.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
namespace TimeProviderExtensions; | ||
|
||
/// <summary> | ||
/// Represents a synthetic time provider that can be used to enable deterministic behavior in tests. | ||
/// </summary> | ||
/// <remarks> | ||
/// Learn more at <see href="https://github.com/egil/TimeProviderExtensions"/>. | ||
/// </remarks> | ||
public partial class ManualTimeProvider : TimeProvider | ||
{ | ||
// The reason this class exists and it is separate from ManualTimer is that | ||
// the GC should be collect the ManualTimer in case users forget to dispose it. | ||
// If all the references captured by this type was part of the ManualTimer | ||
// type, the finalizer would not be invoked on ManualTimer if a callback was scheduled. | ||
private sealed class ManualTimerScheduler : IComparable<ManualTimerScheduler> | ||
{ | ||
private readonly TimerCallback callback; | ||
private readonly object? state; | ||
private readonly ManualTimeProvider timeProvider; | ||
private TimeSpan period; | ||
private bool running; | ||
|
||
public DateTimeOffset CallbackTime { get; set; } | ||
|
||
public ManualTimerScheduler(ManualTimeProvider timeProvider, TimerCallback callback, object? state) | ||
{ | ||
this.timeProvider = timeProvider; | ||
this.callback = callback; | ||
this.state = state; | ||
} | ||
|
||
public int CompareTo(ManualTimerScheduler? other) | ||
=> other is not null | ||
? Comparer<DateTimeOffset>.Default.Compare(CallbackTime, other.CallbackTime) | ||
: -1; | ||
|
||
internal void Cancel() | ||
{ | ||
if (running) | ||
{ | ||
timeProvider.RemoveCallback(this); | ||
} | ||
} | ||
|
||
internal void Change(TimeSpan dueTime, TimeSpan period) | ||
{ | ||
Cancel(); | ||
|
||
this.period = period; | ||
|
||
if (dueTime != Timeout.InfiniteTimeSpan) | ||
{ | ||
ScheduleCallback(dueTime); | ||
} | ||
} | ||
|
||
internal void TimerElapsed() | ||
{ | ||
running = false; | ||
|
||
callback.Invoke(state); | ||
|
||
if (period != Timeout.InfiniteTimeSpan && period != TimeSpan.Zero) | ||
{ | ||
ScheduleCallback(period); | ||
} | ||
} | ||
|
||
private void ScheduleCallback(TimeSpan waitTime) | ||
{ | ||
running = true; | ||
|
||
if (waitTime == TimeSpan.Zero) | ||
{ | ||
TimerElapsed(); | ||
} | ||
else | ||
{ | ||
timeProvider.ScheduleCallback(this, waitTime); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.