Friday, October 28, 2022
HomeWordPress DevelopmentASP.NET: CRON Service Employee - DEV Neighborhood πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

ASP.NET: CRON Service Employee – DEV Neighborhood πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»


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
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
* * * * * *
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode

Instalamos la librerΓ­a Cronos para poder parsear expresiones CRON.

dotnet add package deal Cronos
Enter fullscreen mode

Exit fullscreen mode



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);
}
Enter fullscreen mode

Exit fullscreen mode

  • 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 modo await hasta que hayan transcurrido esos 60 segundos. Este mΓ©todo regresa true si el intervalo se cumpliΓ³ y nadie cancelΓ³ la tarea, regresarΓ‘ false si el stoppingToken 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.
  • 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!;
}
Enter fullscreen mode

Exit fullscreen mode



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;
    }
}
Enter fullscreen mode

Exit fullscreen mode

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;
    }
}
Enter fullscreen mode

Exit fullscreen mode

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();
Enter fullscreen mode

Exit fullscreen mode

Y el resultado:

Image description

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.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments