Async timer in Scheduler Background Service
Asked Answered
B

3

17

I'm writing a hosted service in .Net-Core which runs a job in the background based off of a timer.

Currently I have to code running synchronously like so:

public override Task StartAsync(CancellationToken cancellationToken)
{
    this._logger.LogInformation("Timed Background Service is starting.");

    this._timer = new Timer(ExecuteTask, null, TimeSpan.Zero,
        TimeSpan.FromSeconds(30));

    return Task.CompletedTask;
}

private void ExecuteTask(object state)
{
    this._logger.LogInformation("Timed Background Service is working.");
    using (var scope = _serviceProvider.CreateScope())
    {
        var coinbaseService = scope.ServiceProvider.GetRequiredService<CoinbaseService>();
        coinbaseService.FinalizeMeeting();
    }
}

I'd like to run this Async on the timer but I don't want to run async using fire and forget because my it could cause race conditions in my code. e.g( subscribing to the timer.Elapsed event)

Is there a way I can leverage asynchronous code on a timed schedule without executing fire and forget

Basie answered 19/12, 2018 at 4:27 Comment(2)
Why can't you just private async Task ExecuteTask(object state)?Cuevas
@Cuevas ExecuteTask is fired from the timer, I don't want to fire and forget my task but I after debugging I think its going to happen anywayBasie
C
16

The whole purpose of async is to not hold up the primary threads. But this is a background thread already, so it doesn't really matter - unless it's an ASP.NET Core application. That's the only time it would matter since there is a limited thread pool and exhausting it means that no more requests can be served.

If you really want to run it async, just make it async:

private async void ExecuteTask(object state)
{
    //await stuff here
}

Yes, I know you say you don't want to "fire and forget", but events really are just that: they're fire and forget. So your ExecuteTask method will be called and nothing will care (or check) if it's (1) still running or (2) if it failed. That is true whether you run this async or not.

You can mitigate failures by just wrapping everything your ExecuteTask method in a try/catch block and make sure it's logged somewhere so you know what happened.

The other issue is knowing if it's still running (which, again, is a problem even if you're not running async). There is a way to mitigate that too:

private Task doWorkTask;

private void ExecuteTask(object state)
{
    doWorkTask = DoWork();
}

private async Task DoWork()
{
    //await stuff here
}

In this case, your timer just starts the task. But the difference is that you're keeping a reference to the Task. This would let you check on the status of the Task anywhere else in your code. For example, if you want to verify whether it's done, you can look at doWorkTask.IsCompleted or doWorkTask.Status.

Additionally, when your application shuts down, you can use:

await doWorkTask;

to make sure the task has completed before closing your application. Otherwise, the thread would just be killed, possibly leaving things in an inconsistent state. Just be aware that using await doWorkTask will throw an exception if an unhandled exception happened in DoWork().

It's also probably a good idea to verify if the previous task has completed before starting the next one.

Cocci answered 19/12, 2018 at 5:1 Comment(7)
Async free's up you're threads, I'd like to free up my thread, additionally my service code is written async and I'd not like to run synchrounouslyBasie
It only helps to free up threads in ASP.NET Core. Otherwise, it won't matter. I updated my answer to talk about how to do that and how to handled some issues that, really, would be issues whether this is async or not.Cocci
Thanks, I'll mark this as resolved because it turns out if I use fire and forget or not it will run my tasks concurrently :/, but wouldn't this be relevant to even the older version of .net. especially on shared hosted because there could limit the threads per process via IIS? async uses hardware interrupts so it will free up a thread during network and I/O Operations.Basie
You are right: if you're running this in a web app then yes, you should run it async (whether it's fire-and-forget or not).Cocci
To prevent running the tasks concurrently (I assume you mean the next iteration of the timer starts while the previous one hasn't finished?) then you can restart the timer manually rather than letting it reset by itself. Do this by declaring it as new Timer(ExecuteTask, null, TimeSpan.Zero, -1); then, when your task has finished, call _timer.Change(TimeSpan.FromSeconds(30), -1) to tell it to start counting down.Cocci
But with the fire and forget method isn't there a chance the Task will be garbage collected before it finished?Equivalent
@Equivalent To be honest, I don't know. However, I think the Task object just lets you observe the state of the work being done, and it being destroyed doesn't interrupt the work. But I don't know for sure.Cocci
G
32

For those who are looking for complete example which prevents running tasks concurrently. Based on @Gabriel Luci answer and comments.

Please feel free to comment so I can correct it.

    /// <summary>
    /// Based on Microsoft.Extensions.Hosting.BackgroundService  https://github.com/aspnet/Extensions/blob/master/src/Hosting/Abstractions/src/BackgroundService.cs
    /// Additional info: - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2&tabs=visual-studio#timed-background-tasks
    ///                  - https://mcmap.net/q/588466/-async-timer-in-scheduler-background-service
    /// </summary>

    public abstract class TimedHostedService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private Timer _timer;
        private Task _executingTask;
        private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

        public TimedHostedService(ILogger<TimedHostedService> logger)
        {
            _logger = logger;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is starting.");

            _timer = new Timer(ExecuteTask, null, TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(-1));

            return Task.CompletedTask;
        }

        private void ExecuteTask(object state)
        {
            _timer?.Change(Timeout.Infinite, 0);
            _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
        }

        private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
        {
            await RunJobAsync(stoppingToken);
            _timer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(-1));
        }

        /// <summary>
        /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
        /// </summary>
        /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
        /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
        protected abstract Task RunJobAsync(CancellationToken stoppingToken);

        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is stopping.");
            _timer?.Change(Timeout.Infinite, 0);

            // Stop called without start
            if (_executingTask == null)
            {
                return;
            }

            try
            {
                // Signal cancellation to the executing method
                _stoppingCts.Cancel();
            }
            finally
            {
                // Wait until the task completes or the stop token triggers
                await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
            }

        }

        public void Dispose()
        {
            _stoppingCts.Cancel();
            _timer?.Dispose();
        }
    }
Greenfinch answered 19/6, 2019 at 10:58 Comment(0)
C
16

The whole purpose of async is to not hold up the primary threads. But this is a background thread already, so it doesn't really matter - unless it's an ASP.NET Core application. That's the only time it would matter since there is a limited thread pool and exhausting it means that no more requests can be served.

If you really want to run it async, just make it async:

private async void ExecuteTask(object state)
{
    //await stuff here
}

Yes, I know you say you don't want to "fire and forget", but events really are just that: they're fire and forget. So your ExecuteTask method will be called and nothing will care (or check) if it's (1) still running or (2) if it failed. That is true whether you run this async or not.

You can mitigate failures by just wrapping everything your ExecuteTask method in a try/catch block and make sure it's logged somewhere so you know what happened.

The other issue is knowing if it's still running (which, again, is a problem even if you're not running async). There is a way to mitigate that too:

private Task doWorkTask;

private void ExecuteTask(object state)
{
    doWorkTask = DoWork();
}

private async Task DoWork()
{
    //await stuff here
}

In this case, your timer just starts the task. But the difference is that you're keeping a reference to the Task. This would let you check on the status of the Task anywhere else in your code. For example, if you want to verify whether it's done, you can look at doWorkTask.IsCompleted or doWorkTask.Status.

Additionally, when your application shuts down, you can use:

await doWorkTask;

to make sure the task has completed before closing your application. Otherwise, the thread would just be killed, possibly leaving things in an inconsistent state. Just be aware that using await doWorkTask will throw an exception if an unhandled exception happened in DoWork().

It's also probably a good idea to verify if the previous task has completed before starting the next one.

Cocci answered 19/12, 2018 at 5:1 Comment(7)
Async free's up you're threads, I'd like to free up my thread, additionally my service code is written async and I'd not like to run synchrounouslyBasie
It only helps to free up threads in ASP.NET Core. Otherwise, it won't matter. I updated my answer to talk about how to do that and how to handled some issues that, really, would be issues whether this is async or not.Cocci
Thanks, I'll mark this as resolved because it turns out if I use fire and forget or not it will run my tasks concurrently :/, but wouldn't this be relevant to even the older version of .net. especially on shared hosted because there could limit the threads per process via IIS? async uses hardware interrupts so it will free up a thread during network and I/O Operations.Basie
You are right: if you're running this in a web app then yes, you should run it async (whether it's fire-and-forget or not).Cocci
To prevent running the tasks concurrently (I assume you mean the next iteration of the timer starts while the previous one hasn't finished?) then you can restart the timer manually rather than letting it reset by itself. Do this by declaring it as new Timer(ExecuteTask, null, TimeSpan.Zero, -1); then, when your task has finished, call _timer.Change(TimeSpan.FromSeconds(30), -1) to tell it to start counting down.Cocci
But with the fire and forget method isn't there a chance the Task will be garbage collected before it finished?Equivalent
@Equivalent To be honest, I don't know. However, I think the Task object just lets you observe the state of the work being done, and it being destroyed doesn't interrupt the work. But I don't know for sure.Cocci
M
10

Here is an improved version based on previous responses. Improvements:

  1. Possible exception during task execution is caught and won't prevent next tasks from executing.
  2. For each task executing a scope is created, so you can access any scoped services in RunJobAsync
  3. You can specify interval and initial task execution time in the inherited class.

Access to scoped services example

    protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
            DbContext context = serviceProvider.GetRequiredService<DbContext>();
    }

Source code:

public abstract class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    IServiceProvider _services;
    public TimedHostedService(IServiceProvider services)
    {
        _services = services;
        _logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
        
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));

        return Task.CompletedTask;
    }

    private void ExecuteTask(object state)
    {
        _timer?.Change(Timeout.Infinite, 0);
        _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
    }

    private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
    {
        try
        {
            using (var scope = _services.CreateScope())
            {
                await RunJobAsync(scope.ServiceProvider, stoppingToken);
            }
        }
        catch (Exception exception)
        {
            _logger.LogError("BackgroundTask Failed", exception);
        }
        _timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
    }

    /// <summary>
    /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
    /// </summary>
    /// <param name="serviceProvider"></param>
    /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
    /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
    protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
    protected abstract TimeSpan Interval { get; }
    
    protected abstract TimeSpan FirstRunAfter { get; }
    
    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }

    }

    public void Dispose()
    {
        _stoppingCts.Cancel();
        _timer?.Dispose();
    }
}
Mulholland answered 30/4, 2021 at 18:8 Comment(1)
Suggestion for improvement: Timeout.InfiniteTimeSpan instead of TimeSpan.FromMilliseconds(-1).Josephinajosephine

© 2022 - 2024 — McMap. All rights reserved.