Tareas en segundo plano con Hangfire

En el desarrollo de aplicaciones, surge con frecuencia la necesidad de ejecutar tareas en segundo plano. Esta necesidad se manifiesta especialmente cuando ciertas operaciones pueden demorar algunos segundos, y aún así, deseamos proporcionar una respuesta inmediata al usuario. Imagina un escenario común en un servicio WebApi: al registrar un nuevo usuario, después de almacenar la información en la base de datos, surge la tarea adicional de enviar un correo de bienvenida. En este contexto, detener el flujo de la solicitud para esperar a que se complete el envío del correo no es la opción más eficiente. La clave está en gestionar estas tareas secundarias de manera asincrónica, permitiendo que continúe la ejecución principal mientras se maneja la tarea en segundo plano. Hangfire, una herramienta de gran utilidad, se presenta como una solución que simplifica este proceso al ofrecer un sistema de cola de tareas eficiente y fácil de implementar.

Hangfire se puede usar casi en cualquier aplicación .NET, para este articulo vamos hacer ejemplos usando un minimal API en NET 8. Como se menciono antes, Hangfire almacena las tareas para posteriormente sean procesadas, para esto nos ofrece varias opciones como SQLServer o Redis. Pero para simplificar este ejemplo usaremos el almacenamiento en memoria.

NOTA: En esta publicación se maneja el término «tarea«, para definir un proceso a ejecutar, pero la documentación de Hangfire hace referencia al término, «trabajo», para este articulo es lo mismo concepto.

¿Que es Hangfire?

Hangfire se presenta en su página oficial como una herramienta que simplifica el procesamiento en segundo plano en aplicaciones .NET y .NET Core de manera sencilla y eficiente. Lo que destaca de inmediato es que no requiere servicios de Windows ni procesos independientes adicionales. Además, cuenta con el respaldo de almacenamiento persistente, brindando estabilidad a las operaciones que gestiona.

En términos más simples, Hangfire es una biblioteca diseñada para facilitar la gestión completa de tareas en segundo plano, abordando desde su almacenamiento y ejecución hasta su administración y visualización. Además, es de código abierto y está disponible de forma gratuita o para uso comercial.

¿Como funciona Hangfire?

En la página oficial de Hangfire esta explicado a detalle como funciona, pero en resumen podemos decir que esta compuesto por estos 3 componentes:

  • Cliente: Es la aplicación que se encarga de crear las tareas que se van ejecutar, normalmente es nuestra aplicación la cual requiere realizar procesamiento en segundo plano.
  • Almacenamiento: Este es el espacio donde se almacena las tareas; es el lugar donde el cliente lo guarda para que posteriormente sea procesado por el servidor.
  • Servidor: Es la aplicación que obtiene las tareas del almacenamientos y las ejecuta, normalmente en aplicaciones pequeñas, la misma aplicación cliente puede ser servidor (Este es el caso que vamos a ver en el ejemplo mas adelante).
Esctructura de HangFire
Arquitectura de Hangfire

Configuración Hangfire para un WebApi

Para empezar con Hangfire en nuestro WebApi, el primer paso es instalar los paquetes NuGet requeridos. Debemos incluir librería de Hangfire como paquete obligatorio y Hangfire.MemoryStorage si deseas almacenar las tareas en memoria como va ser nuestro ejemplo. En caso de preferir Redis o SQL Server, instala el paquete correspondiente.

En el archivo Program.cs, procedemos a la configuración básica de Hangfire. Aunque esta biblioteca ofrece diversas opciones de configuración detalladas en su documentación, nos enfocaremos en las configuraciones mínimas necesarias para mantener el ejemplo simple.

Debemos especificar dónde se almacenarán las tareas, en nuestro caso usaremos el almacenamiento en memoria. Además, necesitamos indicar a la aplicación que inicie un servidor Hangfire. Esto significa que nuestro WebApi de ejemplo sera cliente y servidor Hangfire.

builder.Services.AddHangfire(x => x.UseMemoryStorage());
builder.Services.AddHangfireServer();

Listo! Ahora ya podemos empezar a crear nuestras tareas.

Tipo de tareas

Hangfire permite crear varios tipos de tareas, las que se van a explicar a continuación son las que están disponibles en su versión gratuita.

Disparar y olvidar tarea

Una estrategia común en Hangfire es el enfoque «Disparar y olvidar tares», que implica crear una tarea sin esperar su resultado inmediato. En este caso, Hangfire se encarga de almacenar la tarea para su ejecución posterior. Este enfoque resulta útil cuando necesitamos realizar una tarea sin detener el flujo principal del proceso, ya que no dependemos del resultado para continuar.

Consideremos un ejemplo práctico de un endpoint que utiliza una tarea en segundo plano para enviar notificaciones.

app.MapGet("/notificacion", () =>
{
    BackgroundJob.Enqueue(() => SendEmail());
    return "Ok";
});

En este fragmento de código, el envío de correo se invoca como una tarea en segundo plano. Lo que sucede en realidad es que el cliente agrega una nueva tarea al almacenamiento con el código a ejecutar. Posteriormente, el servidor Hangfire tomará esta tarea y lo ejecutará.

Es fundamental destacar que el método Enqueue, una vez que agrega la tarea al almacenamiento, permite que el proceso principal del cliente continúe sin bloquearse. Por ejemplo, si el método SendEmail demora 10 segundos en ejecutarse, el proceso principal del cliente no se ve afectado. El WebApi proporcionará la respuesta al usuario, mientras que el servidor Hangfire ejecutará la tarea en el momento adecuado.

Si prefieres trabajar con inyección de dependencias puedes usar la interfaz IBackgroundJobClient para invocar el método antes mencionado.

Tareas retrasadas

Las «Tareas retrasadas» en Hangfire operan de manera similar a los tareas de «Disparar y olvidar». La distinción principal radica en la capacidad de establecer un tiempo de espera antes de que la tarea comience.

Consideremos un ejemplo práctico donde se utiliza una tarea retrasada para enviar un correo electrónico después de un período de espera de diez segundos:

app.MapGet("/espera", () =>
{                                                                                                                                                                                                                                                                                                                                                        
    BackgroundJob.Schedule(() => SendEmail() TimeSpan.FromSeconds(10));
    return "OK";
});

En este caso, se destaca que el proceso principal no se bloquea durante los diez segundos especificados. Al igual que en el caso anterior, una vez que la tarea se coloca en el almacenamiento, el proceso continúa su ejecución. Después de mínimo 10 segundos, el servidor Hangfire tomará la tarea programada y la ejecutará.

Es importante resaltar el término «mínimo», ya que Hangfire no está revisando constantemente el almacenamiento para buscar tareas pendientes. Por defecto, realiza esta verificación cada 15 segundos (aunque este intervalo puede modificarse). Esta se establece por razones de rendimiento, evitando sobrecargar el servidor. Como resultado, es posible que algunas tareas no se ejecuten exactamente en el momento especificado debido a esto.

Tareas recurrentes

Las «Tareas recurrentes» en Hangfire son similares a lo que comúnmente se conoce como tareas programadas o tareas cron de algunos sistemas operativos. Para implementar este tipo de tareas, utilizamos la función RecurringJob.AddOrUpdate(). Este método nos permite establecer una tarea que se ejecutará periódicamente según la frecuencia y el patrón que definamos.

app.MapGet("/recurrente", () =>
{
    RecurringJob.AddOrUpdate("MyJob", () => SendEmail(), Cron.Minutely);
    return "OK";
});

El funcionamiento de los «Tareas recurrentes» en Hangfire es análogo al de un cron. En este contexto, especificamos el trabajo que deseamos ejecutar y, a continuación, definimos con qué frecuencia debe llevarse a cabo. En el ejemplo anterior, estamos indicando que la tarea se ejecute cada minuto. Es crucial tener en consideración que este intervalo de tiempo no es preciso, ya que está sujeto a las mismas consideraciones que mencionamos para el caso de «tareas retrasadas».

Además, es fundamental establecer un identificador único para la tarea recurrente. En nuestro ejemplo, utilizamos el identificador «MyJob». Este identificador no solo sirve para distinguir la tarea en el momento de su creación, sino que también es el identificador para su posterior modificación. La asignación de un identificador único permite la fácil identificación y gestión de tareas recurrentes a medida que evoluciona la aplicación.

Tareas continuas

Si deseamos ejecutar varias tareas y que se ejecuten en secuencia una después de otra, podemos lograrlo con el método ContinueJobWith.

app.MapGet("/continuation", () =>
{
    var jobId = BackgroundJob.Enqueue(() => SendEmail());
    BackgroundJob.ContinueJobWith(jobId, () => CrearLog();
    return "OK";
});

Cuando se crea una tarea, se devuelve un identificador que podemos usar para tener una referencia. Podemos usar ese identificador para por ejemplo se ejecute la tarea para crear un logs después de que se complete la tarea de enviar correo como se muestra en el ejemplo anterior.

Dashboard de Hangfire

Una de las ventajas destacadas de Hangfire es la inclusión de un dashboard que facilita la visualización y gestión del estado de nuestras tareas, permitiéndonos ejecutar y eliminar tareas directamente desde la interfaz, sin necesidad de realizar modificaciones en la aplicación.

En el caso de una aplicación MVC, activar el dashboard es tan simple como incluir la siguiente línea de código en nuestro archivo Program.cs:

app.UseHangfireDashboard();

Una vez habilitado, podemos acceder al dashboard mediante la URL {URL_APP}/hangfire. Al ingresar, nos encontraremos con una interfaz similar a la siguiente, que proporciona una visión detallada del estado de nuestras tareas, incluso podemos hacer operaciones como ejecutar una tarea manualmente o eliminarlo.

Dashboard de hangfire
Dashboard

Podemos hacer configuraciones adicionales, por ejemplo podemos cambiar la URL, tener 2 dashboard separados entre otras configuraciones.

Conclusión

El uso de tareas en segundo plano puede ayudar a mejorar la eficiencia de nuestras aplicaciones, no importa si son tareas de un único procesamiento o recurrente, Hangfire cubre todo estos aspectos por lo cual debemos tenernos en nuestras herramientas para enriquecer nuestras aplicaciones.

Te invito a ver mis otros artículos.

Referencias