An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
ASP.NET: CRON Service Worker
Introduccin
En este artculo veremos como crear un servicio en segundo plano que se ejecutar segn un programa de intervalos. Este ser expresado como si fuera una tarea CRON de Linux, que son en esencia, tareas programadas.
Nota : Aqu encuentras el cdigo fuente
El formato cron se expresa de la siguiente manera:
Allowed values Allowed special characters Comment second (optional) 0-59 * , - / minute 0-59 * , - / hour 0-23 * , - / day of month 1-31 * , - / L W ? month 1-12 or JAN-DEC * , - / day of week 0-6 or SUN-SAT * , - / # L ? Both 0 and 7 means SUN * * * * * *
Donde 5 o 6 caracteres representan el intervalo de tiempo en el que la tarea se ejecutar, ejemplo:
Expresin | Descripcin |
---|---|
* * * * * | Cada minuto |
0 0 1 * * | A media noche, en da primero de cada mes |
0 0 * * MON-FRI | A las 0:00, de Lunes a Viernes |
Nota : Si quieres conocer ms, puedes visitar este repositorio HangfireIO/Cronnos
Implementacin en ASP.NET Core y Hosted Services
Antes de seguir, realizar este tipo de background services en asp.net core es cada vez ms fcil, tan fcil que ya existen soluciones como HangFire y Azure Functions que realizan este tipo de tareas (tambin basadas en formato CRON). Pero si de igual forma, quisieras aprender hacer tu implementacin (a veces es mejor keep it simple) te recomiendo seguir leyendo .
Para comenzar, crearemos un proyecto web vaco o de consola, da igual ya que no utilizaremos ningn endpoint HTTP, pero puedes mezclarlos sin problema.
dotnet new web -o BackgroundJob.Cron
Instalamos la librera Cronos para poder parsear expresiones CRON.
dotnet add package Cronos
CronBackgroundJob
Esta clase base y abstracta, ser la que se encargar de ejecutar un proceso segn un intervalo de tiempo, este intervalo ser definido como ya lo hemos dicho, con una expresin CRON.
using Cronos;namespace BackgroundJob.Cron.Jobs;public abstract class CronBackgroundJob : BackgroundService{ private PeriodicTimer? _timer; private readonly CronExpression _cronExpression; private readonly TimeZoneInfo _timeZone; public CronBackgroundJob(string rawCronExpression, TimeZoneInfo timeZone) { _cronExpression = CronExpression.Parse(rawCronExpression); _timeZone = timeZone; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { DateTimeOffset? nextOcurrence = _cronExpression.GetNextOccurrence(DateTimeOffset.UtcNow, _timeZone); if (nextOcurrence.HasValue) { var delay = nextOcurrence.Value - DateTimeOffset.UtcNow; _timer = new PeriodicTimer(delay); if (await _timer.WaitForNextTickAsync(stoppingToken)) { _timer.Dispose(); _timer = null; await DoWork(stoppingToken); // Reagendamos await ExecuteAsync(stoppingToken); } } } protected abstract Task DoWork(CancellationToken stoppingToken);}
- PeriodicTimer: Es un nuevo Timer que nos permite "esperar" el siguiente "Tick" del timer. Es decir, si queremos que el timer se ejecute cada 60 segundos, el mtodo
WaitForNextTickAsync
estar en modoawait
hasta que hayan transcurrido esos 60 segundos. Este mtodo regresatrue
si el intervalo se cumpli y nadie cancel la tarea, regresarfalse
si elstoppingToken
cancel la ejecucin.- Creamos el timer con la diferencia de tiempo (
TimeSpan
) entre la fecha y hora actual y la fecha y hora de la siguiente ocurrencia, es decir: Si estamos 27/10/2022 15:00 y la siguiente ocurrencia es el 27/10/2022 16:00, hay una diferencia de 1 hora (3600000 milisegundos), hasta que pase ese tiempo, el PeriodicTimer lanzar su Next Tick.
- Creamos el timer con la diferencia de tiempo (
- CronExpression: Nos ayuda a entender una expresin cron, en este caso tenemos que darle una fecha y un uso horario (este opcional) para que se pueda determinar cundo ser la siguiente ocurrencia (o sea, la siguiente fecha y hora en que se debe de correr la tarea)
GetNextOcurrence
: Regresa un DateTimeOffset con la fecha a futuro en donde toca ya correr la tarea.
- WaitForNextTickAsync: Este mtodo genera un
Task
que se espera hasta que ocurra el siguiente Tick del Timer.- Cada ejecucin liberamos el timer para que en el siguiente ciclo volverlo a crear con la siguiente ocurrencia del Cron, ya que esto no necesariamente ser una espera esttica o igual en cada Tick.
- Un ejemplo es si pongo que corra de lunes a viernes a las 9PM, la ejecucin de jueves a viernes esperar 24 horas, pero de viernes a lunes esperar 72 horas.
- DoWork: Este mtodo abstracto ser el que se ejecutar en cada ocurrencia, es abstracto porque cada Worker que hagamos, har una tarea diferente.
Al terminar de correr el DoWork
de forma recursiva, mandamos a llamar nuevamente la tarea para agendar la siguiente ejecucin, esto durar por siempre o hasta que el stoppingToken diga lo contrario.
CronSettings
Para poder correr el worker/job anterior, debemos de poder tener una expresin cron y aparte el uso horario que se quiera considerar.
namespace BackgroundJob.Cron.Jobs;public class CronSettings<T>{ public string CronExpression { get; set; } = default!; public TimeZoneInfo TimeZone { get; set; } = default!;}
CronBackgroundJobExtensions
Para hacer fcil esta integracin entre los options y cada background job, es mejor crear este mtodo de extensin que nos ayudar a registrar cada dependencia de cada job.
namespace BackgroundJob.Cron.Jobs;public static class CronBackgroundJobExtensions{ public static IServiceCollection AddCronJob<T>(this IServiceCollection services, Action<CronSettings<T>> options) where T: CronBackgroundJob { if (options == null) { throw new ArgumentNullException(nameof(options)); } var config = new CronSettings<T>(); options.Invoke(config); if (string.IsNullOrWhiteSpace(config.CronExpression)) { throw new ArgumentNullException(nameof(CronSettings<T>.CronExpression)); } services.AddSingleton<CronSettings<T>>(config); services.AddHostedService<T>(); return services; }}
Usamos el Options Pattern muy comn en ASP.NET para registrar cada background job que necesitemos.
Es obligatorio que se indique una configuracin por medio de CronSettings<T>
y tambin es obligatorio tener una expresin cron.
MySchedulerJob
Este ser el background job de ejemplo:
namespace BackgroundJob.Cron.Jobs;public class MySchedulerJob : CronBackgroundJob{ private readonly ILogger<MySchedulerJob> _log; public MySchedulerJob(CronSettings<MySchedulerJob> settings, ILogger<MySchedulerJob> log) :base(settings.CronExpression, settings.TimeZone) { _log = log; } protected override Task DoWork(CancellationToken stoppingToken) { _log.LogInformation("Running... at {0}", DateTime.UtcNow); return Task.CompletedTask; }}
Realmente lo nico que hace es escribir en los logs la fecha en la que se ejecut y as poder comprobar que todo funciona.
Program
Para finalizar, registramos las dependencias con la extensin que escribimos y vual, ya podemos correr.
using BackgroundJob.Cron.Jobs;var builder = WebApplication.CreateBuilder(args);builder.Services.AddCronJob<MySchedulerJob>(options => { // Corre cada minuto options.CronExpression = "* * * * *"; options.TimeZone = TimeZoneInfo.Local;});var app = builder.Build();app.Run();
Y el resultado:
Conclusin
A pesar de que ya existen soluciones que nos ayuda implementar este tipo de tareas, mantener las cosas simples a veces es la opcin que necesitas por que la tarea es simple.
Si necesitas algo que escale, que sea resiliente, flexible a un costo de tiempo bajo, definitivamente te recomiendo irte por Azure Functions. Si no ests en Azure, puedes irte por Hangfire.
Pero si lo que necesitas son tareas programadas y no depender de Azure, los Hosted Services es una buena opcin.
Referencias
- Schedule Cron Jobs using HostedService in ASP.NET Core | by Changhui Xu | codeburst
- PeriodicTimer: Temporizadores asncronos en .NET 6 | Variable not found
- HangfireIO/Cronos: Fully-featured .NET library for working with Cron expressions. Built with time zones in mind and intuitively handles daylight saving time transitions (github.com)
- PeriodicTimer Class (System.Threading) | Microsoft Learn
Original Link: https://dev.to/isaacojeda/aspnet-cron-service-worker-2no9
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To