Achtergrond taken met queue vanaf ASP.NET Core 2.1

Vanaf versie 2.1 kan je achtergrond taken uitvoeren op drie verschillende manieren:

  • Een timer gebaseerde achtergrond taak, die op bepaalde tijden werk uitvoert
  • Een scoped service achtergrond taak, het verschil is dat je hier dependency injection kan gebruiken
  • Een queued (rij) achtergrond taak, die ik hier verder bespreek.

Waarom een taak op een achtergrond rij zetten

Vaak heb je bepaalde taken die door de gebruiker geïnitieerd worden, maar die lang duren, en je liever wil loskoppelen van het gebruikers interface. Of anders gezegd, waar je de gebruiker niet op wil laten wachten.

Je moet dan van te voren bedenken wat je doet als er iets fout gaat, de gebruiker wacht niet op het resultaat, dus die kan je ook niet makkelijk informeren. Alternatieven zijn bijvoorbeeld:

  • Je informeert de support organisatie zodat die het probleem kan herstellen
  • Je informeert de gebruiker achteraf, bijvoorbeeld door iets in de database te zetten, dat ergens op een pagina wordt gezet,
  • Je informeert de gebruiker in real-time met SignalR (communicatie rechtstreeks naar de browser)

Hoe dan ook, als je iets in de achtergrond uitvoert (bijvoorbeeld het versturen van een of meer mails), dan kan je niet meteen in de response van de pagina een fout boodschap opnemen.

Maar als je je dat goed realiseert, is het toch handig om je gebruikersinterface te versnellen door de lang lopende zaken er uit te halen.

Je kan dan op ieder moment dat je wil een async task op de queue zetten die dan op de achtergrond wordt uitgevoerd. Daarover later meer.

Je begint met het opnemen van de volgende twee klassen in je project, deze code is van Microsoft:

BackgroundTaskQueue.cs

 //this implements the queue, using QueueBackgroundWorkItem(task) a workitem
    //is placed on the queue,and then processed
    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;
        }
    }

QueuedHostService.cs

public class QueuedHostedService : BackgroundService
{
    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 cancellationToken)
    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        while (!cancellationToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(cancellationToken);

            try
            {
                await workItem(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                   $"Error occurred executing {nameof(workItem)}.");
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }
}

Vervolgens registreer je beide in ConfigureServices:

//add the hosted background service and queue
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services.AddHostedService<QueuedHostedService>();

En nu kan je aan de slag met je background taken!

In je controller zorg je dat de dependency injection de queue klaarzet: voeg IBackgroundTaskQueue queue toe als een van de parameters in de controller constructor, en bewaar de queue in een private variabele:

private readonly IBackgroundTaskQueue _queue;

Nu kan je langlopende taken in de queue zetten:

_queue.QueueBackgroundWorkItem(async token =>
{
    await EmailHandler.SendFinishEmail(tblSession, testId, clientName, examiner);
});

Als je op deze manier iets op de queue zet, gaat de code meteen verder. De gebruiker hoeft dus niet te wachten tot het is afgehandeld.

Het workitem dat je op de queue hebt gezet wordt meteen uitgevoerd door een andere process thread. Als er meerdere workitems tegelijk (door verschillende gebruikers, of snel achterelkaar) op de queue gezet worden, dan handelt de background queue ze een voor een af, in volgorde waarop je ze op de queue hebt gezet. Kortom, ze worden één voor één uitgevoerd in plaats van allemaal tegelijk.

Cannot access a disposed object

Als je deze fout krijgt, heb je een parameter gebruikt die ondertussen is disposed. Bij mij was dat _AppSettings. Die wordt geïnjecteerd in mijn controller, maar weer opgeruimd als de controller klaar is. Als je achtergrond taak dan nog loopt krijg je netjes deze fout.

Ik heb daarom de configuratie parameters in een static variabele gezet, dan kan ik er altijd bij. Het goede nieuws is dat je rustig parameters kan meegeven aan je background task, maar het slechte nieuws is dat je wel moet opletten of die waarden niet disposed zijn. Dus geen geïnjecteerde instances doorgeven, een ook niet HTTPContext, tenzij je de controller laat wachten op het achtergrond resultaat, zodat je zeker bent dat de geïnjecteerde services nog bestaan.

Het is ook belangrijk te realiseren dat je geen headers aan de response kan toevoegen nadat de HttpResponse is gestart.

Commentaar? Ik hoor het graag.

Laat een reactie achter

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *