Альтернативное решение для HostingEnvironment.QueueBackgroundWorkItem в .NET Core

Мы работаем с .NET Core Web Api и ищем легкое решение для записи запросов с переменной интенсивностью в базу данных, но не хотим, чтобы клиент ждал процесса сохранения.
К сожалению, в dnx не реализовано HostingEnvironment.QueueBackgroundWorkItem(..), и Task.Run(..) небезопасно.
Есть ли элегантное решение?


person Notanywho Notanywho    schedule 29.04.2016    source источник
comment
Почему отрицательный голос? Мне кажется, это очень хороший вопрос. QueueBackgroundWorkItem, безусловно, очень полезен.   -  person Augusto Barreto    schedule 29.04.2016
comment
HostingEnvironment.QueueBackgroundWorkItem тоже не был в безопасности. Это было менее опасно, чем Task.Run, но все же небезопасно.   -  person Stephen Cleary    schedule 18.10.2016
comment
Хороший вопрос. Я сам пытаюсь реализовать отчет о прогрессе signalR (используя интерфейс IProgress), но из-за асинхронного характера SignalR мне нужно обрабатывать отчеты о ходе выполнения как задачи (хотя и очень недолговечные задачи), не замедляя работу, о которой они сообщают. .   -  person Shazi    schedule 15.10.2017
comment
В случае webapi вы можете просто использовать Response.OnCompleted(Func‹Task›), который добавляет делегата, который будет вызываться после завершения ответа.   -  person Larsi    schedule 10.03.2020


Ответы (7)


QueueBackgroundWorkItem больше нет, но у нас есть IApplicationLifetime вместо IRegisteredObject, который используется прежним. И, на мой взгляд, для таких сценариев это выглядит довольно многообещающе.

Идея (и я до сих пор не совсем уверен, если она довольно плохая, поэтому будьте осторожны!) заключается в регистрации синглтона, который порождает и новые задачи. Кроме того, внутри этого синглтона мы можем зарегистрировать «остановленное событие», чтобы правильно ожидать все еще выполняющиеся задачи.

Эту «концепцию» можно использовать для коротких операций, таких как ведение журнала, отправка почты и тому подобное. Вещи, которые не должны занимать много времени, но могут привести к ненужным задержкам для текущего запроса.

public class BackgroundPool
{
    protected ILogger<BackgroundPool> Logger { get; }

    public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
    {
        if (logger == null)
            throw new ArgumentNullException(nameof(logger));
        if (lifetime == null)
            throw new ArgumentNullException(nameof(lifetime));

        lifetime.ApplicationStopped.Register(() =>
        {
            lock (currentTasksLock)
            {
                Task.WaitAll(currentTasks.ToArray());
            }

            logger.LogInformation(BackgroundEvents.Close, "Background pool closed.");
        });

        Logger = logger;
    }

    private readonly object currentTasksLock = new object();

    private readonly List<Task> currentTasks = new List<Task>();

    public void SendStuff(Stuff whatever)
    {
        var task = Task.Run(async () =>
        {
            Logger.LogInformation(BackgroundEvents.Send, "Sending stuff...");

            try
            {
                // do THE stuff

                Logger.LogInformation(BackgroundEvents.SendDone, "Send stuff returns.");
            }
            catch (Exception ex)
            {
                Logger.LogError(BackgroundEvents.SendFail, ex, "Send stuff failed.");
            }
        });

        lock (currentTasksLock)
        {
            currentTasks.Add(task);

            currentTasks.RemoveAll(t => t.IsCompleted);
        }
    }
}

Такой BackgroundPool должен быть зарегистрирован как синглтон и может использоваться любым другим компонентом через DI. В настоящее время я использую его для отправки почты, и он отлично работает (также тестировал отправку почты во время закрытия приложения).

Примечание. доступ к таким элементам, как текущий HttpContext, в фоновой задаче работать не должен. старое решение все равно использует UnsafeQueueUserWorkItem, чтобы запретить это.

Что вы думаете?

Обновление:

В ASP.NET Core 2.0 появились новые возможности для фоновых задач, которые стали лучше в ASP.NET Core 2.1: Реализация фоновых задач в веб-приложениях или микросервисах .NET Core 2.x с помощью IHostedService и класса BackgroundService

person Axel Heer    schedule 03.04.2017
comment
В вашем делегате ApplicationStopped.Register вы на самом деле не ждете, пока задача вернется из Task.WaitAll(currentTask.ToArray());. Делать этот звонок бессмысленным. - person Shazi; 15.10.2017

Обновление, декабрь 2019 г. ASP.NET Core 3.0 поддерживает простой способ реализации фоновых задач с помощью Microsoft.NET.Sdk.Worker. Это отлично и работает очень хорошо.

Как упомянул @axelheer IHostedService — это то, что нужно для .NET Core 2.0 и более поздних версий.

Мне нужна была легкая замена ASP.NET Core для HostingEnvironment.QueueBackgroundWorkItem, поэтому я написал DalSoft.Hosting .BackgroundQueue, который использует IHostedService.

PM>Установка пакета DalSoft.Hosting.BackgroundQueue

В вашем ASP.NET Core Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
   services.AddBackgroundQueue(onException:exception =>
   {

   });
}

Чтобы поставить фоновую задачу в очередь, просто добавьте BackgroundQueue в конструктор вашего контроллера и вызовите Enqueue.

public EmailController(BackgroundQueue backgroundQueue)
{
   _backgroundQueue = backgroundQueue;
}

[HttpPost, Route("/")]
public IActionResult SendEmail([FromBody]emailRequest)
{
   _backgroundQueue.Enqueue(async cancellationToken =>
   {
      await _smtp.SendMailAsync(emailRequest.From, emailRequest.To, request.Body);
   });

   return Ok();
}
person DalSoft    schedule 07.01.2018
comment
зарегистрирована ли BackgroundQueue как синглтон? - person Ivan-Mark Debono; 26.03.2020
comment
Да, но я бы посмотрел на Microsoft.NET.Sdk.Worker, так как он делает все, что делает мой пакет, и даже больше. - person DalSoft; 26.03.2020
comment
@DalSoft Вы упомянули Microsoft.NET.Sdk.Worker, но я не совсем понимаю, как это прямая замена HostingEnvironment.QueueBackgroundWorkItem. Не могли бы вы опубликовать новый ответ, описывающий, как это выглядит? Спасибо! - person Todd Menier; 17.09.2020
comment
@DalSoft, да, я тоже в замешательстве, не могли бы вы пролить свет. я получаю urs, но как Worker делает все, что мой пакет, я не вижу, не могли бы вы объяснить. - person Seabizkit; 06.10.2020
comment
Извините, ребята, смотрите Поставленные в очередь фоновые задачи Документация MS немного изменилась. Если вы все еще считаете, что библиотека-обертка стоит того, дайте мне знать. - person DalSoft; 06.10.2020

Вы можете использовать Hangfire (http://hangfire.io/) для фоновых заданий в .NET Core.

Например :

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));
person ycrumeyrolle    schedule 02.09.2016
comment
Для этого решения требуется SQL Server. Не то чтобы он не мог заменить некоторые вещи, которые вы могли бы делать с HostingEnvironment.QueueBackgroundWorkItem, но это значительно более тяжелое решение, о котором, я думаю, стоит здесь упомянуть. - person Todd Menier; 17.09.2020
comment
@ToddMenier Hangfire может работать со многими различными решениями для хранения, включая Redis и новое хранилище в памяти, над которым они сейчас работают. - person Mason G. Zhwiti; 07.07.2021

Вот измененная версия ответа Акселя, которая позволяет вам передавать делегатов и выполнять более агрессивную очистку завершенных задач.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;

namespace Example
{
    public class BackgroundPool
    {
        private readonly ILogger<BackgroundPool> _logger;
        private readonly IApplicationLifetime _lifetime;
        private readonly object _currentTasksLock = new object();
        private readonly List<Task> _currentTasks = new List<Task>();

        public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
        {
            if (logger == null)
                throw new ArgumentNullException(nameof(logger));
            if (lifetime == null)
                throw new ArgumentNullException(nameof(lifetime));

            _logger = logger;
            _lifetime = lifetime;

            _lifetime.ApplicationStopped.Register(() =>
            {
                lock (_currentTasksLock)
                {
                    Task.WaitAll(_currentTasks.ToArray());
                }

                _logger.LogInformation("Background pool closed.");
            });
        }

        public void QueueBackgroundWork(Action action)
        {
#pragma warning disable 1998
            async Task Wrapper() => action();
#pragma warning restore 1998

            QueueBackgroundWork(Wrapper);
        }

        public void QueueBackgroundWork(Func<Task> func)
        {
            var task = Task.Run(async () =>
            {
                _logger.LogTrace("Queuing background work.");

                try
                {
                    await func();

                    _logger.LogTrace("Background work returns.");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.HResult, ex, "Background work failed.");
                }
            }, _lifetime.ApplicationStopped);

            lock (_currentTasksLock)
            {
                _currentTasks.Add(task);
            }

            task.ContinueWith(CleanupOnComplete, _lifetime.ApplicationStopping);
        }

        private void CleanupOnComplete(Task oldTask)
        {
            lock (_currentTasksLock)
            {
                _currentTasks.Remove(oldTask);
            }
        }
    }
}
person Scott Chamberlain    schedule 23.05.2017
comment
Как и в ответе Акселя, вы на самом деле не ждете, пока задача вернется из Task.WaitAll(currentTask.ToArray());. - person Shazi; 15.10.2017

Я знаю, что это немного поздно, но мы только что столкнулись с этой проблемой. Итак, после прочтения множества идей, вот решение, к которому мы пришли.

    /// <summary>
    /// Defines a simple interface for scheduling background tasks. Useful for UnitTesting ASP.net code
    /// </summary>
    public interface ITaskScheduler
    {
        /// <summary>
        /// Schedules a task which can run in the background, independent of any request.
        /// </summary>
        /// <param name="workItem">A unit of execution.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
        void QueueBackgroundWorkItem(Action<CancellationToken> workItem);

        /// <summary>
        /// Schedules a task which can run in the background, independent of any request.
        /// </summary>
        /// <param name="workItem">A unit of execution.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
        void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    }


    public class BackgroundTaskScheduler : BackgroundService, ITaskScheduler
    {
        public BackgroundTaskScheduler(ILogger<BackgroundTaskScheduler> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogTrace("BackgroundTaskScheduler Service started.");

            _stoppingToken = stoppingToken;

            _isRunning = true;
            try
            {
                await Task.Delay(-1, stoppingToken);
            }
            catch (TaskCanceledException)
            {
            }
            finally
            {
                _isRunning = false;
                _logger.LogTrace("BackgroundTaskScheduler Service stopped.");
            }
        }

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

            if (!_isRunning)
                throw new Exception("BackgroundTaskScheduler is not running.");

            _ = Task.Run(() => workItem(_stoppingToken), _stoppingToken);
        }

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

            if (!_isRunning)
                throw new Exception("BackgroundTaskScheduler is not running.");

            _ = Task.Run(async () =>
                {
                    try
                    {
                        await workItem(_stoppingToken);
                    }
                    catch (Exception e)
                    {
                        _logger.LogError(e, "When executing background task.");
                        throw;
                    }
                }, _stoppingToken);
        }

        private readonly ILogger _logger;
        private volatile bool _isRunning;
        private CancellationToken _stoppingToken;
    }

ITaskScheduler (который мы уже определили в нашем старом клиентском коде ASP.NET для целей тестирования UTest) позволяет клиенту добавить фоновую задачу. Основная цель BackgroundTaskScheduler — захватить токен отмены остановки (который принадлежит хосту) и передать его всем фоновым Task; который по определению работает в System.Threading.ThreadPool, поэтому нет необходимости создавать собственный.

Чтобы правильно настроить размещенные службы, см. #62478535">этот пост.

Наслаждаться!

person Kabuo    schedule 19.06.2020
comment
Это говорит рабочим элементам остановить свою работу (через CancellationToken), но на самом деле не гарантирует завершение любых запущенных рабочих элементов до завершения ExecuteAsync, верно? - person StriplingWarrior; 03.02.2021
comment
@StriplingWarrior код работает так же, как ASP.NET, в том смысле, что поставленные в очередь фоновые задачи будут выполняться от начала до конца, пока IIS не будет закрыт. Если IIS закрывается, маркер отмены будет подписан, и, таким образом, будут уведомлены все запущенные в данный момент задачи. - person Kabuo; 04.02.2021
comment
Одно из улучшений @StriplingWarrior может состоять в том, чтобы сохранить набор запущенных задач и не возвращаться из ExecuteAsync() до тех пор, пока все текущие задачи не будут корректно завершены. - person Kabuo; 04.02.2021
comment
ASP.NET QueueBackgroundWorkItem использует BackgroundWorkScheduler, много проблем, чтобы убедиться, что его IRegisteredObject отменяется только после завершения последнего рабочего элемента. - person StriplingWarrior; 04.02.2021

Я использовал Quartz.NET (не требует SQL Server) со следующим методом расширения, чтобы легко установить и запустить работу:

public static class QuartzUtils
{
        public static async Task<JobKey> CreateSingleJob<JOB>(this IScheduler scheduler,
            string jobName, object data) where JOB : IJob
        {
            var jm = new JobDataMap { { "data", data } };

            var jobKey = new JobKey(jobName);

            await scheduler.ScheduleJob(
                JobBuilder.Create<JOB>()
                .WithIdentity(jobKey)
                .Build(),

                TriggerBuilder.Create()
                .WithIdentity(jobName)
                .UsingJobData(jm)
                .StartNow()
                .Build());

            return jobKey;
        }
}

Данные передаются как объект, который должен быть сериализуемым. Создайте IJob, который обрабатывает задание следующим образом:

public class MyJobAsync :IJob
{
   public async Task Execute(IJobExecutionContext context)
   {
          var data = (MyDataType)context.MergedJobDataMap["data"];
          ....

Выполнить так:

await SchedulerInstance.CreateSingleJob<MyJobAsync>("JobTitle 123", myData);
person Hakakou    schedule 17.02.2021

Оригинальный HostingEnvironment.QueueBackgroundWorkItem был однострочным и очень удобным в использовании. «Новый» способ сделать это в ASP Core 2.x требует чтения страниц загадочной документации и написания значительного объема кода.

Чтобы избежать этого, вы можете использовать следующий альтернативный метод

    public static ConcurrentBag<Boolean> bs = new ConcurrentBag<Boolean>();

    [HttpPost("/save")]
    public async Task<IActionResult> SaveAsync(dynamic postData)
    {

    var id = (String)postData.id;

    Task.Run(() =>
                {
                    bs.Add(Create(id));
                });

     return new OkResult();

    }


    private Boolean Create(String id)
    {
      /// do work
      return true;
    }

Статический ConcurrentBag<Boolean> bs будет содержать ссылку на объект, это не позволит сборщику мусора собрать задачу после возврата контроллера.

person user11658885    schedule 17.06.2019