Always use CancellationToken in PeriodicTimer!

.NET 6 introduced a new timer class called PeriodicTimer. Unlike traditional timers that rely on callbacks, PeriodicTimer waits asynchronously for timer ticks, making it a great alternative if you want to avoid the potential issues associated with callbacks.

To create a new instance of PeriodicTimer, simply pass a single argument: Period, which specifies the time interval in milliseconds between each invocation.

var everySecondTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));

while (await everySecondTimer.WaitForNextTickAsync())
{
     // put your cron job logic here
}

It has a WaitForNextTickAsync method which is used to get another tick to run. All good and general!

Another use-case for PeriodicTimer is in BackgroundService where we may need to put our code which then runs at the startup of ASP.NET Core API, consider the below code:

var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(1));

var periodicTimer = new PeriodicTimer(TimeSpan.FromSeconds(30));

Console.WriteLine($"Started at: {TimeProvider.System.GetLocalNow()}");

while (!cts.IsCancellationRequested && await periodicTimer.WaitForNextTickAsync())
{
    Console.WriteLine("------->Running in the while loop");
    await Task.Delay(2000);
}

Console.WriteLine($"Finished at: {TimeProvider.System.GetLocalNow()}");

I this code I used the a CancellationTokenSource to show the case, we can use the stoppingToken injected in the ExecuteAsync though.

So, we assume the after 1 second the cancellation token will be cancelled, by knowing so, how long the while loop will it take to be finished?

Let's run the code and see what prints on console:

Started at: 8/9/2024 10:44:36 PM +07:00
------->Running in the while loop
Finished at: 8/9/2024 10:45:08 PM +07:00

Ooops!

We except the while will end after 1 second but it takes 32 seconds to be done! what? how?

If we check the while condition again: !cts.IsCancellationRequested && await periodicTimer.WaitForNextTickAsync(), and we know until this condition is true the while will iterate. So the first part is true immediately because the cancellation token is still valid and the runtime will move on to check another condition WaitForNextTickAsync and here the issue comes into the picture 😃

Every time we call WaitForNextTickAsync it takes whatever to passed as Periodic parameter on creating instance of periodic timer and for our timer is 30 seconds, then it waits for 30 seconds to return true and run time will move in into while scope and executes the Task.Delay(2000) but in next iterate and condition will be false and app will be done in 32s!

What is the issue here? Yes the problem is not using CancellationToken! Then:

var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(1));

var periodicTimer = new PeriodicTimer(TimeSpan.FromSeconds(30));

Console.WriteLine($"Started at: {TimeProvider.System.GetLocalNow()}");
try
{
    while (!cts.IsCancellationRequested && await periodicTimer.WaitForNextTickAsync(cts.Token))
    {
        Console.WriteLine("------->Running in the while loop");
        await Task.Delay(2000);
    }
}
catch
{
    //ignore to log message
}

Console.WriteLine($"Finished at: {TimeProvider.System.GetLocalNow()}");

What about console log:

Started at: 8/9/2024 10:49:49 PM +07:00
Finished at: 8/9/2024 10:49:50 PM +07:00

Perfect!

What we did here is use CancellationToken when calling WaitForNextTickAsync it causes the end the method right after cancelling the CancellationToken.

Why it matters? Imagine using periodic timer without using cancellation token, there are some places where you're app will struggling with WaitForNextTickAsync:

  • Application Shutdown: In case of using it in a BackgroundService or HostedService it may cause to spending more time on app shutdown even though the cancellation token is requested.
  • Dispose method: If there is an awaiting periodic task in the dispose method it causes the application to go in fatal error as well as some of the code won't run in the dispose as result of app shutdown timeout!

The default application shutdown is 30 seconds which means if there is periodic timer taking more than that will be stopped by force at runtime.

Code Simple!

1
An error has occurred. This application may no longer respond until reloaded. Reload x