Hangfire es una biblioteca para .net (core), que permite la ejecución asíncrona de algún código según el principio de "disparar y olvidar". Un ejemplo de dicho código puede ser el envío de correo electrónico, procesamiento de video, sincronización con otro sistema, etc. Además de "disparar y olvidar", hay soporte para tareas diferidas, así como tareas programadas en formato Cron.
Actualmente, hay muchas bibliotecas de este tipo. Algunos de los beneficios de Hangfire son:
- Configuración simple, API conveniente
- Fiabilidad Hangfire garantiza que la tarea creada se ejecutará al menos una vez
- Capacidad para realizar tareas en paralelo y excelente rendimiento
- Extensibilidad (aquí la usaremos a continuación)
- Documentación bastante completa y comprensible
- Panel de control en el que puede ver todas las estadísticas sobre las tareas
No entraré en demasiados detalles, ya que hay muchos buenos artículos sobre Hangfire y cómo usarlo. En este artículo discutiré cómo usar el soporte de varias colas (o grupos de tareas), cómo arreglar la funcionalidad de reintento estándar y hacer que cada cola tenga una configuración individual.
Soporte existente para (pseudo) colas
Nota importante: en el título, utilicé el término pseudo-cola porque Hangfire no garantiza que las tareas se realizarán en un orden específico. Es decir El principio de "Primero en entrar, primero en salir" no se aplica y no confiaremos en él. Además, el autor de la biblioteca recomienda que las tareas sean idempotentes, es decir estable contra ejecución múltiple imprevista. Además, usaré solo la palabra "cola", porque Hangfire usa el término "Cola".
Hangfire tiene soporte de cola simple. Aunque no ofrece la flexibilidad de los sistemas de cola de mensajes, como rabbitMQ o Azure Service Bus, a menudo es suficiente para resolver una amplia gama de tareas.
Cada tarea tiene la propiedad "Cola", es decir, el nombre de la cola en la que debe ejecutarse. De manera predeterminada, la tarea se envía a la cola con el nombre "predeterminado" a menos que se especifique lo contrario. Se necesita soporte para múltiples colas para gestionar por separado la ejecución de tareas de diferentes tipos. Por ejemplo, podríamos querer que las tareas de procesamiento de video caigan en la cola "video_queue" y envíen correos electrónicos a la cola "email_queue". Por lo tanto, podemos realizar de forma independiente estos dos tipos de tareas. Si queremos mover el procesamiento de video a un servidor dedicado, podemos hacerlo fácilmente ejecutando un servidor Hangfire separado como una aplicación de consola que procesará la cola "video_queue".
Pasemos a practicar
La configuración del servidor Hangfire en asp.net core es la siguiente:
public void Configure(IApplicationBuilder app) { app.UseHangfireServer(new BackgroundJobServerOptions { WorkerCount = 2, Queues = new[] { "email_queue", "video_queue" } }); }
Problema 1: las tareas de reproducción entran en la cola predeterminada
Como mencioné anteriormente, hay una cola predeterminada en Hangfire llamada "predeterminada". Si una tarea colocada en la cola, por ejemplo, "video_queue", falló y necesita ser reintentada, se enviará nuevamente a la cola "predeterminada" y no "video_queue" y, como resultado, nuestra tarea no se realizará en absoluto La instancia del servidor Hangfire que nos gustaría, si es que lo desea. Este comportamiento fue establecido por mí experimentalmente y es probablemente un error en el propio Hangfire.
Filtros de trabajo
Hangfire nos brinda la posibilidad de expandir la funcionalidad con la ayuda de los llamados filtros (filtros de trabajo ), que son similares en principio a los filtros de acciones en ASP.NET MVC. El hecho es que la lógica interna de Hangfire se implementa como una máquina de estado. Este es un motor que transfiere secuencialmente las tareas en el grupo de un estado a otro (por ejemplo, creado -> en cola -> procesado -> exitoso), y los filtros nos permiten "interceptar" la tarea que se ejecuta cada vez que cambia su estado y manipularla. Un filtro se implementa como un atributo que se puede aplicar a un único método, clase o globalmente.
Parámetros de trabajo
El objeto ElectStateContext se pasa como argumento al método de filtro. Este objeto contiene información completa sobre la tarea actual. Entre otras cosas, tiene los métodos GetJobParameter <> (...) y SettJobParameter <> (...). Los parámetros de trabajo le permiten guardar información relacionada con una tarea en una base de datos. Es en los Parámetros del trabajo donde se almacena el nombre de la cola a la que se envió originalmente la tarea, solo por alguna razón esta información se ignora durante el próximo reintento.
Solución
Entonces, tenemos una tarea que terminó en error y debería enviarse para volver a ejecutarla en la cola correcta (en la misma que se le asignó en el momento de la creación inicial). La repetición de una tarea que se completó con un error es una transición del estado "fallido" al estado "en cola". Para resolver el problema, cree un filtro que, cuando la tarea ingrese al estado "en cola", verifique en qué cola se envió la tarea inicialmente y ponga el parámetro "QueueName" en el valor deseado:
public class HangfireUseCorrectQueueFilter : JobFilterAttribute, IElectStateFilter { public void OnStateElection(ElectStateContext context) { if (context.CandidateState is EnqueuedState enqueuedState) { var queueName = context.GetJobParameter<string>("QueueName"); if (string.IsNullOrWhiteSpace(queueName)) { context.SetJobParameter("QueueName", enqueuedState.Queue); } else { enqueuedState.Queue = queueName; } } } }
Para aplicar el filtro predeterminado a todas las tareas (es decir, globalmente), agregue el siguiente código a nuestra configuración:
GlobalJobFilters.Filters.Add(new HangfireUseCorrectQueueFilter { Order = 1 });
Otro pequeño inconveniente es que la colección GlobalJobFilters por defecto contiene una instancia de la clase AutomaticRetryAttribute. Este es un filtro estándar que se encarga de volver a ejecutar las tareas fallidas. También envía la tarea a la cola "predeterminada", ignorando la cola original. Para que nuestra bicicleta circule, debe eliminar este filtro de la colección y dejar que nuestro filtro se responsabilice de las tareas repetidas. Como resultado, el código de configuración se verá así:
var defaultRetryFilter = GlobalJobFilters.Filters .FirstOrDefault(f => f.Instance is AutomaticRetryAttribute); if (defaultRetryFilter != null && defaultRetryFilter.Instance != null) { GlobalJobFilters.Filters.Remove(defaultRetryFilter.Instance); } GlobalJobFilters.Filters.Add(new HangfireUseCorrectQueueFilter { Order = 1 });
Cabe señalar que AutomaticRetryAttribute implementa la lógica de aumentar automáticamente el intervalo entre intentos (el intervalo aumenta con cada intento posterior), y al eliminar AutomaticRetryAttribute de la colección GlobalJobFilters, abandonamos esta funcionalidad (consulte la implementación del método ScheduleAgainLater )
Por lo tanto, hemos logrado que nuestras tareas se puedan realizar en diferentes colas, y esto nos permite gestionar de forma independiente su ejecución, incluido el procesamiento de diferentes colas en diferentes máquinas. Solo que ahora no sabemos cuántas veces y en qué intervalo se repetirán nuestras tareas en caso de error, ya que eliminamos AutomaticRetryAttribute de la colección de filtros.
Problema 2: configuraciones individuales para cada cola
Queremos poder configurar el intervalo y el número de repeticiones por separado para cada cola, y también, si para alguna cola no especificamos valores explícitamente, queremos que se apliquen los valores predeterminados. Para hacer esto, implementamos otro filtro y lo llamamos HangfireRetryJobFilter
.
Idealmente, el código de configuración debería verse así:
GlobalJobFilters.Filters.Add(new HangfireRetryJobFilter { Order = 2, ["email_queue"] = new HangfireQueueSettings { DelayInSeconds = 120, RetryAttempts = 3 }, ["video_queue"] = new HangfireQueueSettings { DelayInSeconds = 60, RetryAttempts = 5 } });
Solución
Para hacer esto, primero agregue la clase HangfireQueueSettings
, que servirá como contenedor para nuestra configuración.
public sealed class HangfireQueueSettings { public int RetryAttempts { get; set; } public int DelayInSeconds { get; set; } }
Luego agregamos la implementación del filtro en sí, que, cuando las tareas se repiten después de un error, aplicará la configuración según la configuración de la cola y supervisará el número de reintentos:
public class HangfireRetryJobFilter : JobFilterAttribute, IElectStateFilter, IApplyStateFilter { private readonly HangfireQueueSettings _defaultQueueSettings = new HangfireQueueSettings { RetryAttempts = 3, DelayInSeconds = 10 }; private readonly IDictionary<string, HangfireQueueSettings> _settings = new Dictionary<string, HangfireQueueSettings>(); public HangfireQueueSettings this[string queueName] { get { return _settings.TryGetValue(queueName, out HangfireQueueSettings queueSettings) ? queueSettings : _defaultQueueSettings; } set { _settings[queueName] = value; } } public void OnStateElection(ElectStateContext context) { if (!(context.CandidateState is FailedState failedState)) {
Nota para el código: al implementar la clase HangfireRetryJobFilter
, se tomó como base la clase AutomaticRetryAttribute
de HangfireRetryJobFilter
, por lo tanto, la implementación de algunos métodos coincide parcialmente con los métodos correspondientes de esta clase.
Problema 3: ¿cómo enviar una tarea a una cola específica?
Logré encontrar dos formas de asignar la tarea a la cola: documentada y - no.
1er método : cuelgue el atributo correspondiente en el método
[Queue("video_queue")] public void SomeMethod() { } BackgroundJob.Enqueue(() => SomeMethod());
http://docs.hangfire.io/en/latest/background-processing/configuring-queues.html
Segundo método (no documentado): use la clase BackgroundJobClient
var client = new BackgroundJobClient(); client.Create(() => MyMethod(), new EnqueuedState("video_queue"));
La ventaja del segundo método es que no crea dependencias innecesarias en Hangfire y le permite decidir durante qué proceso debe realizarse la tarea. Desafortunadamente, en la documentación oficial, no encontré mención de la clase BackgroundJobClient
y cómo aplicarla. Utilicé el segundo método en mi solución, por lo que se prueba en la práctica.
Conclusión
En este artículo, utilizamos el soporte de múltiples colas en Hangfire para separar el procesamiento de diferentes tipos de tareas. Implementamos nuestro mecanismo para repetir tareas completadas sin éxito con la posibilidad de una configuración individual para cada cola, expandiendo la funcionalidad de Hangfire usando los filtros de trabajo, y también aprendimos cómo enviar tareas a la cola deseada para su ejecución.
Espero que este artículo sea útil para alguien. Estaré encantado de comentar.
Enlaces utiles
Documentación de Hangfire
Código fuente de Hangfire
Scott Hanselman - Cómo ejecutar tareas en segundo plano en ASP.NET