Async timer in Scheduler Background Service

johnny 5 picture johnny 5 · Dec 19, 2018 · Viewed 9.3k times · Source

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

Answer

g36 picture g36 · Jun 19, 2019

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://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2&tabs=visual-studio#timed-background-tasks
    ///                  - https://stackoverflow.com/questions/53844586/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();
        }
    }