Scheduled task on .Net Core

Viewed 9674

I need to convert a couple of scheduled tasks from the Scheduler task of widows server to a standalone application using .Net.

In the past I've used Quartz on .Net framework 4.x, having some small issue with multiple long running tasks based on different schedulers.

Now I'm using .Net 5 and I'm wondering if there is a new way to schedule tasks, like the worker service or it's still better and more flexible to use Quartz.Net.

Since I need to run long time tasks, from 30s to 2 hrs, I need to create a timed background task, using System.Threading.Timer

The code should be the following:

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoSomething, null, TimeSpan.Zero, 
            TimeSpan.FromHours(24));

        return Task.CompletedTask;
    }

and it should call DoSomething every 24 hrs.

My doubts are:

  • When does it start to work and to count the 24hrs, when I first run the application?
  • How can I say that the task must be run at a specific time in a day, at midnight for example?
  • Is the Worker Service suitable for managing scheduled tasks?
1 Answers

When does it start to work and to count the 24hrs, when I first run the application?

Yes. Set a breakpoint and start your application. You'll see how quickly it fires.

Is the Worker Service suitable for managing scheduled tasks?

Yes.

How can I say that the task must be run at a specific time in a day, at midnight for example?

Let's take a look at this code:

public sealed class MyTimedBackgroundService : BackgroundService
{

    private static int SecondsUntilMidnight()
    {
        return (int)(DateTime.Today.AddDays(1.0) - DateTime.Now).TotalSeconds;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var countdown = SecondsUntilMidnight();

        while (!stoppingToken.IsCancellationRequested)
        {
            if (countdown-- <= 0)
            {
                try
                {
                    await OnTimerFiredAsync(stoppingToken);
                }
                catch(Exception ex)
                {
                    // TODO: log exception
                }
                finally
                {
                    countdown = SecondsUntilMidnight();
                }
            }
            await Task.Delay(1000, stoppingToken);
        }
    }

    private async Task OnTimerFiredAsync(CancellationToken stoppingToken)
    {
        // do your work here
        Debug.WriteLine("Simulating heavy I/O bound work");
        await Task.Delay(2000, stoppingToken);
    }
}

This doesn't use System.Threading.Timer at all incase you are worried about the timer never actually firing off because of some boundary. Some people are paranoid of this. I have never had that happen to me. And I use Timer a lot for this type of work.

It will calculates the number of seconds until midnight then loop until it gets there.

This is a non-reentrant timer and there will be slight time slippage due to the business logic for processing the delay.

Here is another example using System.Threading.Timer:

public sealed class MyTimedBackgroundService : IHostedService
{
    private Timer _t;

    private static int MilliSecondsUntilMidnight()
    {
        return (int)(DateTime.Today.AddDays(1.0) - DateTime.Now).TotalMilliseconds;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // set up a timer to be non-reentrant
        _t = new Timer(async _ => await OnTimerFiredAsync(cancellationToken),
            null, MilliSecondsUntilMidnight(), Timeout.Infinite);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _t?.Dispose();
        return Task.CompletedTask;
    }

    private async Task OnTimerFiredAsync(CancellationToken cancellationToken)
    {
        try
        {
            // do your work here
            Debug.WriteLine("Simulating heavy I/O bound work");
            await Task.Delay(2000, cancellationToken);
        }
        finally
        {
            // set timer to fire off again
            _t?.Change(MilliSecondsUntilMidnight(), Timeout.Infinite);
        }
    }
}

(this code was not tested, there may be some spelling/syntax errors)

This is a non-reentrant timer meaning you are guaranteed that it will not fire off again if it's currently processing data.

It will calculates the number of milliseconds until midnight then set a timer based on that calculation.

This idea was taken from Microsoft.

Both of these examples can be injected as so:

services.AddHostedService<MyTimedBackgroundService>();

Cloud Native Warning:

Keep in mind that since these examples are local to your application, that if your application scales up horizontally where you have more than one instance running, you will be running two or more timers, just in separate processes. Just a friendly reminder. If your application will never scale, then ignore this warning.

Related