asp.net core - job - Solución alternativa a HostingEnvironment.QueueBackgroundWorkItem en.NET Core
hangfire recurring job (4)
Estamos trabajando con .NET Core Web Api y estamos buscando una solución liviana para registrar solicitudes con intensidad variable en la base de datos, pero no queremos que los clientes esperen el proceso de guardado.
Desafortunadamente, no hay HostingEnvironment.QueueBackgroundWorkItem(..)
implementado en dnx
, y Task.Run(..)
no es seguro.
¿Hay alguna solución elegante?
Aquí hay una versión modificada de la respuesta de Axel que le permite pasar delegados y realizar una limpieza más agresiva de las tareas completadas.
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);
}
}
}
}
Como @axelheer mencionó, IHostedService es el camino a seguir en .NET Core 2.0 y superior.
Necesitaba una versión liviana similar a la de ASP.NET Core para HostingEnvironment.QueueBackgroundWorkItem, así que escribí DalSoft.Hosting.BackgroundQueue que usa .NET Core 2.0 IHostedService .
PM> Install-Package DalSoft.Hosting.BackgroundQueue
En su Startup.cs Core ASP.NET:
public void ConfigureServices(IServiceCollection services)
{
services.AddBackgroundQueue(onException:exception =>
{
});
}
Para poner en cola una tarea en segundo plano, simplemente agregue BackgroundQueue
al constructor de su controlador y llame a 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();
}
Puede usar Hangfire ( http://hangfire.io/ ) para trabajos en segundo plano en .NET Core.
Por ejemplo :
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
QueueBackgroundWorkItem
se ha ido, pero tenemos IApplicationLifetime
lugar de IRegisteredObject
, que está siendo utilizado por el anterior. Y parece bastante prometedor para tales escenarios, creo.
La idea (y todavía no estoy muy segura, si es bastante mala; por lo tanto, ¡cuidado!) Es registrar un singleton, que genera y observa nuevas tareas. Dentro de ese singleton podemos, además, registrar un "evento detenido" para poder esperar adecuadamente las tareas aún en ejecución.
Este "concepto" podría usarse para tareas de ejecución corta como el registro, el envío de correo y similares. Cosas, eso no debería tomar mucho tiempo, pero produciría demoras innecesarias para la solicitud actual.
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);
}
}
}
Dicho BackgroundPool
debe registrar como un singleton y puede ser utilizado por cualquier otro componente a través de DI. Actualmente lo estoy usando para enviar correos y funciona bien (envío de correo comprobado durante el cierre de la aplicación).
Nota: el acceso a elementos como el actual HttpContext
dentro de la tarea en segundo plano no debería funcionar. La solución anterior usa UnsafeQueueUserWorkItem
para prohibir eso de todos modos.
¿Qué piensas?
Actualizar:
Con ASP.NET Core 2.0 hay cosas nuevas para tareas en segundo plano, que mejoran con ASP.NET Core 2.1: IHostedService