How to run an async task daily in a Kestrel process?

How do I run an async task in a Kestrel process with a very long time interval (say daily or perhaps even longer)? The task needs to run in the memory space of the web server process to update some global variables that slowly go out of date.

Bad answers:

  • Trying to use an OS scheduler is a poor plan.
  • Calling await from a controller is not acceptable. The task is slow.
  • The delay is too long for Task.Delay() (about 16 hours or so and Task.Delay will throw).
  • HangFire, etc. make no sense here. It’s an in-memory job that doesn’t care about anything in the database. Also, we can’t call the database without a user context (from a logged-in user hitting some controller) anyway.
  • System.Threading.Timer. It’s reentrant.

Bonus:

  • The task is idempotent. Old runs are completely irrelevant.
  • It doesn’t matter if a particular page render misses the change; the next one will get it soon enough.
  • As this is a Kestrel server we’re not really worried about stopping the background task. It’ll stop when the server process goes down anyway.
  • The task should run once immediately on startup. This should make coordination easier.

Some people are missing this. The method is async. If it wasn’t async the problem wouldn’t be difficult.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

I am going to add an answer to this, because this is the only logical way to accomplish such a thing in ASP.NET Core: an IHostedService implementation.

This is a non-reentrant timer background service that implements IHostedService.

public sealed class MyTimedBackgroundService : IHostedService
{
    private const int TimerInterval = 5000; // change this to 24*60*60 to fire off every 24 hours
    private Timer _t;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Requirement: "fire" timer method immediatly.
        await OnTimerFiredAsync();

        // set up a timer to be non-reentrant, fire in 5 seconds
        _t = new Timer(async _ => await OnTimerFiredAsync(),
            null, TimerInterval, Timeout.Infinite);
    }

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

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

So, I know we discussed this in comments, but System.Threading.Timer callback method is considered a Event Handler. It is perfectly acceptable to use async void in this case since an exception escaping the method will be raised on a thread pool thread, just the same as if the method was synchronous. You probably should throw a catch in there anyway to log any exceptions.

You brought up timers not being safe at some interval boundary. I looked high and low for that information and could not find it. I have used timers on 24 hour intervals, 2 day intervals, 2 week intervals… I have never had them fail. I have a lot of them running in ASP.NET Core in production servers for years, too. We would have seen it happen by now.

OK, so you still don’t trust System.Threading.Timer

Let’s say that, no… There is just no fricken way you are going to use a timer. OK, that’s fine… Let’s go another route. Let’s move from IHostedService to BackgroundService (which is an implementation of IHostedService) and simply count down.

This will alleviate any fears of the timer boundary, and you don’t have to worry about async void event handlers. This is also a non-reentrant for free.

public sealed class MyTimedBackgroundService : BackgroundService
{
    private const long TimerIntervalSeconds = 5; // change this to 24*60 to fire off every 24 hours

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Requirement: "fire" timer method immediatly.
        await OnTimerFiredAsync(stoppingToken);

        var countdown = TimerIntervalSeconds;

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

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

A bonus side-effect is you can use long as your interval, allowing you more than 25 days for the event to fire as opposed to Timer which is capped at 25 days.

You would inject either of these as so:

services.AddHostedService<MyTimedBackgroundService>();


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x