En este artΓculo veremos como crear un servicio en segundo plano que se ejecutarΓ‘ segΓΊn 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 cΓ³digo fuente
El formato cron se expresa de la siguiente manera:
Allowed values Allowed particular characters Remark
ββββββββββββββ second (elective) 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 ? Each 0 and seven means SUN
β β β β β β
* * * * * *
Donde 5 o 6 caracteres representan el intervalo de tiempo en el que la tarea se ejecutarΓ‘, ejemplo:
ExpresiΓ³n | DescripciΓ³n |
---|---|
* * * * * | Cada minuto |
0 0 1 * * | A media noche, en dΓa primero de cada mes |
0 0 * * MON-FRI | A las 0:00, de Lunes a Viernes |
Nota π‘: Si quieres conocer mΓ‘s, puedes visitar este repositorio HangfireIO/Cronnos
ImplementaciΓ³n en ASP.NET Core y Hosted Providers
Antes de seguir, realizar este tipo de background companies en asp.web core es cada vez mΓ‘s fΓ‘cil, tan fΓ‘cil que ya existen soluciones como HangFire y Azure Capabilities que realizan este tipo de tareas (tambiΓ©n basadas en formato CRON). Pero si de igual forma, quisieras aprender hacer tu implementaciΓ³n (a veces es mejor hold it easy) te recomiendo seguir leyendo π€.
Para comenzar, crearemos un proyecto net vacΓo o de consola, da igual ya que no utilizaremos ningΓΊn endpoint HTTP, pero puedes mezclarlos sin problema.
dotnet new net -o BackgroundJob.Cron
Instalamos la librerΓa Cronos para poder parsear expresiones CRON.
dotnet add package deal Cronos
CronBackgroundJob
Esta clase base y abstracta, serΓ‘ la que se encargarΓ‘ de ejecutar un proceso segΓΊn un intervalo de tiempo, este intervalo serΓ‘ definido como ya lo hemos dicho, con una expresiΓ³n CRON.
utilizing Cronos;
namespace BackgroundJob.Cron.Jobs;
public summary class CronBackgroundJob : BackgroundService
{
Β Β non-public PeriodicTimer? _timer;
Β Β non-public readonly CronExpression _cronExpression;
Β Β non-public readonly TimeZoneInfo _timeZone;
Β Β public CronBackgroundJob(string rawCronExpression, TimeZoneInfo timeZone)
Β Β {
Β Β Β Β _cronExpression = CronExpression.Parse(rawCronExpression);
Β Β Β Β _timeZone = timeZone;
Β Β }
Β Β protected override async Process ExecuteAsync(CancellationToken stoppingToken)
Β Β {
Β Β Β Β DateTimeOffset? nextOcurrence = _cronExpression.GetNextOccurrence(DateTimeOffset.UtcNow, _timeZone);
Β Β Β Β if (nextOcurrence.HasValue)
Β Β Β Β { Β Β Β Β Β
Β Β Β Β Β Β var delay = nextOcurrence.Worth - DateTimeOffset.UtcNow; Β
Β Β Β Β Β Β _timer = new PeriodicTimer(delay);
Β Β Β Β Β Β if (await _timer.WaitForNextTickAsync(stoppingToken))
Β Β Β Β Β Β { Β Β Β Β Β Β
Β Β Β Β Β Β Β Β _timer.Dispose();
Β Β Β Β Β Β Β Β _timer = null;
Β Β Β Β Β Β Β Β await DoWork(stoppingToken);
Β Β Β Β Β Β Β Β // Reagendamos
Β Β Β Β Β Β Β Β await ExecuteAsync(stoppingToken);
Β Β Β Β Β Β }
Β Β Β Β }
Β Β }
Β Β protected summary Process 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 mΓ©todo
WaitForNextTickAsync
estarΓ‘ en modoawait
hasta que hayan transcurrido esos 60 segundos. Este mΓ©todo regresatrue
si el intervalo se cumpliΓ³ y nadie cancelΓ³ la tarea, regresarΓ‘false
si elstoppingToken
cancelΓ³ la ejecuciΓ³n.- Creamos el timer con la diferencia de tiempo (
TimeSpan
) entre la fecha y hora precise 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 Subsequent Tick.
- Creamos el timer con la diferencia de tiempo (
-
CronExpression: Nos ayuda a entender una expresiΓ³n cron, en este caso tenemos que darle una fecha y un uso horario (este opcional) para que se pueda determinar cuΓ‘ndo 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 mΓ©todo genera un
Process
que se espera hasta que ocurra el siguiente Tick del Timer.- Cada ejecuciΓ³n 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 estΓ‘tica o igual en cada Tick.
- Un ejemplo es si pongo que corra de lunes a viernes a las 9PM, la ejecuciΓ³n de jueves a viernes esperarΓ‘ 24 horas, pero de viernes a lunes esperarΓ‘ 72 horas.
- DoWork: Este mΓ©todo abstracto serΓ‘ el que se ejecutarΓ‘ en cada ocurrencia, es abstracto porque cada Employee 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 ejecuciΓ³n, esto durarΓ‘ por siempre o hasta que el stoppingToken diga lo contrario.
CronSettings
Para poder correr el employee/job anterior, debemos de poder tener una expresiΓ³n 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 fΓ‘cil esta integraciΓ³n entre los choices y cada background job, es mejor crear este mΓ©todo de extensiΓ³n 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 companies, Motion<CronSettings<T>> choices)
the place T: CronBackgroundJob
{
if (choices == null)
{
throw new ArgumentNullException(nameof(choices));
}
var config = new CronSettings<T>();
choices.Invoke(config);
if (string.IsNullOrWhiteSpace(config.CronExpression))
{
throw new ArgumentNullException(nameof(CronSettings<T>.CronExpression));
}
companies.AddSingleton<CronSettings<T>>(config);
companies.AddHostedService<T>();
return companies;
}
}
Usamos el Choices Sample muy comΓΊn en ASP.NET para registrar cada background job que necesitemos.
Es obligatorio que se indique una configuraciΓ³n por medio de CronSettings<T>
y tambiΓ©n es obligatorio tener una expresiΓ³n cron.
MySchedulerJob
Este serΓ‘ el background job de ejemplo:
namespace BackgroundJob.Cron.Jobs;
public class MySchedulerJob : CronBackgroundJob
{
non-public readonly ILogger<MySchedulerJob> _log;
public MySchedulerJob(CronSettings<MySchedulerJob> settings, ILogger<MySchedulerJob> log)
:base(settings.CronExpression, settings.TimeZone)
{
_log = log;
}
protected override Process DoWork(CancellationToken stoppingToken)
{
_log.LogInformation("Working... at {0}", DateTime.UtcNow);
return Process.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 extensiΓ³n que escribimos y vualΓ‘, ya podemos correr.
utilizing BackgroundJob.Cron.Jobs;
var builder = WebApplication.CreateBuilder(args);
builder.Providers.AddCronJob<MySchedulerJob>(choices =>
{
// Corre cada minuto
choices.CronExpression = "* * * * *";
choices.TimeZone = TimeZoneInfo.Native;
});
var app = builder.Construct();
app.Run();
Y el resultado:
A pesar de que ya existen soluciones que nos ayuda implementar este tipo de tareas, mantener las cosas simples a veces es la opciΓ³n que necesitas por que la tarea es easy.
Si necesitas algo que escale, que sea resiliente, versatile a un costo de tiempo bajo, definitivamente te recomiendo irte por Azure Capabilities. Si no estΓ‘s en Azure, puedes irte por Hangfire.
Pero si lo que necesitas son tareas programadas y no depender de Azure, los Hosted Providers es una buena opciΓ³n.