Created
April 4, 2026 11:22
-
-
Save neon-sunset/0ffe9fa8cd17210ed6f41e7310569e06 to your computer and use it in GitHub Desktop.
Correct by construction wrapper for PeriodicTimer
This file contains hidden or 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
| /// <summary> | |
| /// Represents a periodic cycle that executes a callback at regular intervals using PeriodicTimer. | |
| /// </summary> | |
| public sealed class Cycle : IAsyncDisposable | |
| { | |
| readonly Task cycleTask; | |
| readonly CancellationTokenSource cancellation; | |
| volatile bool disposed; | |
| Cycle(TimeSpan period, Action callback, TimeSpan? initialDelay, Action<Exception>? onError) | |
| { | |
| ArgumentNullException.ThrowIfNull(callback); | |
| cancellation = new CancellationTokenSource(); | |
| cycleTask = onError == null | |
| ? Run(callback, initialDelay, period, cancellation.Token) | |
| : RunWithErrorHandler(callback, initialDelay, period, onError, cancellation.Token); | |
| } | |
| Cycle(TimeSpan period, Func<CancellationToken, ValueTask> callback, TimeSpan? initialDelay, Action<Exception>? onError) | |
| { | |
| ArgumentNullException.ThrowIfNull(callback); | |
| cancellation = new CancellationTokenSource(); | |
| cycleTask = onError == null | |
| ? Run(callback, initialDelay, period, cancellation.Token) | |
| : RunWithErrorHandler(callback, initialDelay, period, onError, cancellation.Token); | |
| } | |
| /// <summary> | |
| /// Starts a new periodic cycle with a synchronous callback. | |
| /// </summary> | |
| /// <param name="period">The time interval between callback executions.</param> | |
| /// <param name="callback">The callback to execute on each cycle.</param> | |
| /// <param name="initialDelay">Optional initial delay before the first execution. If null, waits for one period.</param> | |
| /// <returns>A new Cycle instance.</returns> | |
| public static Cycle Start( | |
| TimeSpan period, | |
| Action callback, | |
| TimeSpan? initialDelay = null) | |
| { | |
| return new Cycle(period, callback, initialDelay, null); | |
| } | |
| /// <summary> | |
| /// Starts a new periodic cycle with an asynchronous callback. | |
| /// </summary> | |
| /// <param name="period">The time interval between callback executions.</param> | |
| /// <param name="callback">The asynchronous callback to execute on each cycle.</param> | |
| /// <param name="initialDelay">Optional initial delay before the first execution. If null, waits for one period.</param> | |
| /// <returns>A new Cycle instance.</returns> | |
| public static Cycle Start( | |
| TimeSpan period, | |
| Func<CancellationToken, ValueTask> callback, | |
| TimeSpan? initialDelay = null) | |
| { | |
| return new Cycle(period, callback, initialDelay, null); | |
| } | |
| /// <summary> | |
| /// Starts a new periodic cycle with a synchronous callback. | |
| /// </summary> | |
| /// <param name="period">The time interval between callback executions.</param> | |
| /// <param name="callback">The callback to execute on each cycle.</param> | |
| /// <param name="initialDelay">Optional initial delay before the first execution. If null, waits for one period.</param> | |
| /// <param name="onError">Optional callback to handle exceptions from the main callback. If null, exceptions will terminate the cycle.</param> | |
| /// <returns>A new Cycle instance.</returns> | |
| public static Cycle Start( | |
| TimeSpan period, | |
| Action callback, | |
| TimeSpan? initialDelay, | |
| Action<Exception>? onError) | |
| { | |
| return new Cycle(period, callback, initialDelay, onError); | |
| } | |
| /// <summary> | |
| /// Starts a new periodic cycle with an asynchronous callback. | |
| /// </summary> | |
| /// <param name="period">The time interval between callback executions.</param> | |
| /// <param name="callback">The asynchronous callback to execute on each cycle.</param> | |
| /// <param name="initialDelay">Optional initial delay before the first execution. If null, waits for one period.</param> | |
| /// <param name="onError">Optional callback to handle exceptions from the main callback. If null, exceptions will terminate the cycle.</param> | |
| /// <returns>A new Cycle instance.</returns> | |
| public static Cycle Start( | |
| TimeSpan period, | |
| Func<CancellationToken, ValueTask> callback, | |
| TimeSpan? initialDelay, | |
| Action<Exception>? onError) | |
| { | |
| return new Cycle(period, callback, initialDelay, onError); | |
| } | |
| static async Task Run( | |
| Action callback, | |
| TimeSpan? initialDelay, | |
| TimeSpan period, | |
| CancellationToken ct) | |
| { | |
| await Task.Delay(initialDelay ?? period, ct).ConfigureAwait(false); | |
| using var timer = new PeriodicTimer(period); | |
| do callback(); | |
| while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false)); | |
| } | |
| static async Task Run( | |
| Func<CancellationToken, ValueTask> callback, | |
| TimeSpan? initialDelay, | |
| TimeSpan period, | |
| CancellationToken ct) | |
| { | |
| await Task.Delay(initialDelay ?? period, ct).ConfigureAwait(false); | |
| using var timer = new PeriodicTimer(period); | |
| do await callback(ct).ConfigureAwait(false); | |
| while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false)); | |
| } | |
| static async Task RunWithErrorHandler( | |
| Action callback, | |
| TimeSpan? initialDelay, | |
| TimeSpan period, | |
| Action<Exception> onError, | |
| CancellationToken ct) | |
| { | |
| await Task.Delay(initialDelay ?? period, ct).ConfigureAwait(false); | |
| using var timer = new PeriodicTimer(period); | |
| do | |
| { | |
| try { callback(); } | |
| catch (Exception ex) when (!ct.IsCancellationRequested) | |
| { | |
| onError(ex); | |
| } | |
| } | |
| while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false)); | |
| } | |
| static async Task RunWithErrorHandler( | |
| Func<CancellationToken, ValueTask> callback, | |
| TimeSpan? initialDelay, | |
| TimeSpan period, | |
| Action<Exception> onError, | |
| CancellationToken ct) | |
| { | |
| await Task.Delay(initialDelay ?? period, ct).ConfigureAwait(false); | |
| using var timer = new PeriodicTimer(period); | |
| do | |
| { | |
| try { await callback(ct).ConfigureAwait(false); } | |
| catch (Exception ex) when (!ct.IsCancellationRequested) | |
| { | |
| onError(ex); | |
| } | |
| } | |
| while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false)); | |
| } | |
| /// <summary> | |
| /// Stops the cycle and disposes all resources asynchronously. | |
| /// </summary> | |
| public async ValueTask DisposeAsync() | |
| { | |
| if (disposed) | |
| return; | |
| disposed = true; | |
| cancellation.Cancel(); | |
| await cycleTask | |
| .WaitAsync(TimeSpan.FromSeconds(30)) | |
| .ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); | |
| cancellation.Dispose(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment