15

Slender answered my original question about what happens to fire and forget, after the HTTP Response is sent, but Now I'm left with the question how to properly queue background tasks

EDIT

As we all know Async void is generally bad, except for in the case when it comes to event handlers, I would like to execute some background logic without have to have the client wait. My original Idea was to use Fire and Forget

Say I have an event:

public event EventHandler LongRunningTask;

And then someone subscribes a fire and forget task:

LongRunningTask += async(s, e) => { await LongNetworkOperation;};

the web api method is call:

[HttpGet]
public async IActionResult GetTask()
{
    LongRunningTask?.Invoke(this, EventArgs.Empty);
    return Ok();
}

But If I do this my long running task isn't guaranteed to finish, How can I handle running background task without affect the time the time it take to make my request (e.g I don't want to wait for the task to finish first)?

johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • 2
    Neither is it guaranteed to run to completion, nor will it be disposed. Webservers aren't the best place to execute long-running jobs, and you need to be aware of the consequences of the webserver pulling the plug on your code (if, for instance, you're running in IIS and the app-pool hosting your code gets recycled), and also the impact of running "side-jobs" on your sever's ability to actually serve pages in a timely fashion. – spender Jun 30 '18 at 14:24
  • @spender, so those tasks will leak memory, the only reason I wanted to do this is for my websocket, if o need to broadcast to 1000s of clients after an update I’d prefer the update to occur and return instead of waiting for all of them to have to send – johnny 5 Jun 30 '18 at 15:16
  • The comment from @spender may have applied for .NET framework, but with NET Core, there is the built-in BackgroundService, or you can implement your own IHostedService where the host can be a Console App, or Windows Service, or Web Server/Service.. etc. – joedotnot Apr 25 '23 at 18:00

2 Answers2

31

.NET Core 2.1 has an IHostedService, which will safely run tasks in the background. I've found an example in the documentation for QueuedHostedService which I've modified to use the BackgroundService.

public class QueuedHostedService : BackgroundService
{
   
    private Task _backgroundTask;
    private readonly ILogger _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILoggerFactory loggerFactory)
    {
        TaskQueue = taskQueue;
        _logger = loggerFactory.CreateLogger<QueuedHostedService>();
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected async override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (false == stoppingToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(stoppingToken);
            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex, $"Error occurred executing {nameof(workItem)}.");
            }
        }
    }
}

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
        new ConcurrentQueue<Func<CancellationToken, Task>>();

    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _workItems.Enqueue(workItem);
        _signal.Release();
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItems.TryDequeue(out var workItem);

        return workItem;
    }
}

Now we can safely queue up tasks in the background without affecting the time it takes to respond to a request.

janw
  • 8,758
  • 11
  • 40
  • 62
johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • Can I use the same approach to run background task on UI click? , when a user click a button on View for instance?! – Ahmed Elbatt Nov 15 '18 at 09:09
  • In mvc? You can trigger an action on the server that runs a background task – johnny 5 Nov 15 '18 at 13:33
  • How to Dequeue a specific work item? Assume that I've added a lot of work items before. @johnny5 – Thiện Sinh Jan 09 '19 at 16:04
  • @ThiệnSinh, ExecuteAsync automatically dequeues work when it executes you want explicitly Dequeue a work item before it executes? – johnny 5 Jan 09 '19 at 17:00
  • @johnny5 yes, in some case i want explicitly Dequeue a work item before it done. Can you give me sample code. – Thiện Sinh Jan 14 '19 at 03:41
  • Are you doing this from the same request? Or do you need to dequeue from from future requests – johnny 5 Jan 14 '19 at 04:20
  • @johnny5 of course from future requests. The idea is to start a background task and be able to stop it if the user wants(manually). Note that I have many tasks already define. – Thiện Sinh Feb 11 '19 at 12:42
  • @johnny5 How do I cancel the task using CancellationToken? For example, there are 10 tasks queued and I want to cancel 5th task. – vishwas-trivedi May 15 '19 at 02:23
  • Just create a key that represents the task that's is executing and when you want to remove it pass it to dequueue – johnny 5 May 15 '19 at 02:56
  • If I understand this correctly, would this code only execute one task at a time and then only get another item once that first task is complete? How would I use this approach if my service can handle up to 10 tasks at a time (just store task as a collection?) – Geekn Aug 02 '19 at 20:00
  • @Geekn, The cpu can only handle 1 operation at a time. If task are have network operations you can make them async. If you want to run multiple concurrently, I'm not sure. I don't know enough about the hosted services to say anything. My best guess would be to spawn another hosted service. You cannot create threads as they are subject to be shut down, – johnny 5 Aug 03 '19 at 18:32
  • So are you saying the logic in a task cannot create additional task along with passing in the cancellation token? I understand that they could be shut down, but wouldn't that just signal the cancellation token for the child tasks to shutdown as well? – Geekn Aug 04 '19 at 01:08
  • @Geekn there is no problem with queuing up additional tasks they're just going to run sync, if you want to run them concurrently you can remove the semtex, but there you're not guaranteed the order of execution – johnny 5 Aug 04 '19 at 13:26
  • Does it work with queued functions, that make use of services injected with DI? I think when the service dependency is `scoped`, and the request is done - you'll get null reference exception because all dependencies are disposed. – user963935 May 07 '21 at 07:54
  • @user963935 you need to push the dependencies into the service. e.g (if the action depends on claims, the claims should be pushed into a container and then you can reuse them with the task you're trying to execute). As for other things such as scoped services (which do not depend on user specific data that comes from HTTPContext etc) Then you can inject them using DI to the hosted service – johnny 5 May 07 '21 at 14:34
23

Just wanted to add some additional notes to @johnny5 answer. Right now you can use https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/ instead of ConcurrentQueue with Semaphore. The code will be something like this:

public class HostedService: BackgroundService
{
        private readonly ILogger _logger;
        private readonly ChannelReader<Stream> _channel;

        public HostedService(
            ILogger logger,
            ChannelReader<Stream> channel)
        {
            _logger = logger;
            _channel = channel;
        }

        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            await foreach (var item in _channel.ReadAllAsync(cancellationToken))
            {
                try
                {
                    // do your work with data
                }
                catch (Exception e)
                {
                    _logger.Error(e, "An unhandled exception occured");
                }
            }
        }
}

[ApiController]
[Route("api/data/upload")]
public class UploadController : ControllerBase
{
    private readonly ChannelWriter<Stream> _channel;

    public UploadController (
        ChannelWriter<Stream> channel)
    {
        _channel = channel;
    }

    public async Task<IActionResult> Upload([FromForm] FileInfo fileInfo)
    {
        var ms = new MemoryStream();
        await fileInfo.FormFile.CopyToAsync(ms);
        await _channel.WriteAsync(ms);
        return Ok();
    }
}
flerka
  • 341
  • 2
  • 4