Мы работаем с .NET Core Web Api и ищем легкое решение для записи запросов с переменной интенсивностью в базу данных, но не хотим, чтобы клиент ждал процесса сохранения.
К сожалению, в dnx
не реализовано HostingEnvironment.QueueBackgroundWorkItem(..)
, и Task.Run(..)
небезопасно.
Есть ли элегантное решение?
Альтернативное решение для HostingEnvironment.QueueBackgroundWorkItem в .NET Core
Ответы (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
Обновление, декабрь 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();
}
HostingEnvironment.QueueBackgroundWorkItem
. Не могли бы вы опубликовать новый ответ, описывающий, как это выглядит? Спасибо!
- person Todd Menier; 17.09.2020
Вы можете использовать Hangfire (http://hangfire.io/) для фоновых заданий в .NET Core.
Например :
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
HostingEnvironment.QueueBackgroundWorkItem
, но это значительно более тяжелое решение, о котором, я думаю, стоит здесь упомянуть.
- person Todd Menier; 17.09.2020
Вот измененная версия ответа Акселя, которая позволяет вам передавать делегатов и выполнять более агрессивную очистку завершенных задач.
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);
}
}
}
}
Я знаю, что это немного поздно, но мы только что столкнулись с этой проблемой. Итак, после прочтения множества идей, вот решение, к которому мы пришли.
/// <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">этот пост.
Наслаждаться!
ExecuteAsync
, верно?
- person StriplingWarrior; 03.02.2021
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);
Оригинальный 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
будет содержать ссылку на объект, это не позволит сборщику мусора собрать задачу после возврата контроллера.
HostingEnvironment.QueueBackgroundWorkItem
тоже не был в безопасности. Это было менее опасно, чемTask.Run
, но все же небезопасно. - person Stephen Cleary   schedule 18.10.2016