Based .net core task scheduling cron expression
Intro
The last time we implemented a simple task Timer based on the timing of the details can be seen in this article .
But the course of gradually found that this approach may not be appropriate and, in some tasks may only want to perform a certain period of time, it is not only the timer so flexible, I hope you can specify as a cron expression as quartz Specifies the execution time of the task.
cron expression describes
cron common in Unix and Unix-like the operating system among the provided instructions to be executed periodically. The command instructions read from the standard input device, and store it in the "crontab" file for read and executed later. The word comes from the Greek chronos (χρόνος), the intent is the time.
Typically, the
crontab
instructions are stored daemon activated,crond
often run in the background every minute to check for scheduled jobs need to be performed. Such work is generally known as cron Jobs .
cron can be more accurate description of the execution time of periodic tasks, the standard cron expressions are five:
30 4 * * ?
Five values respectively correspond to the position of the minute / hour / date / month / week (day of week)
There are a number of extensions, has 6 bits, and 7 bits, 6 bits corresponding to the expression of a second, corresponding to the first seven seconds, the last one corresponding to the year
0 0 12 * * ?
12:00 daily
0 15 10 ? * *
10:15 daily
0 15 10 * * ?
10:15 daily
30 15 10 * * ? *
10:15:30 day
0 15 10 * * ? 2005
2005 at 10:15 every day
For more information refer to: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
.NET Core CRON service
CRON parsing library using https://github.com/HangfireIO/Cronos
, supports five / six, does not support parsing of the year (7)
Based on BackgroundService
the CRON timed service, implemented as follows:
public abstract class CronScheduleServiceBase : BackgroundService
{
/// <summary>
/// job cron trigger expression
/// refer to: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
/// </summary>
public abstract string CronExpression { get; }
protected abstract bool ConcurrentAllowed { get; }
protected readonly ILogger Logger;
private readonly string JobClientsCache = "JobClientsHash";
protected CronScheduleServiceBase(ILogger logger)
{
Logger = logger;
}
protected abstract Task ProcessAsync(CancellationToken cancellationToken);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
{
var next = CronHelper.GetNextOccurrence(CronExpression);
while (!stoppingToken.IsCancellationRequested && next.HasValue)
{
var now = DateTimeOffset.UtcNow;
if (now >= next)
{
if (ConcurrentAllowed)
{
_ = ProcessAsync(stoppingToken);
next = CronHelper.GetNextOccurrence(CronExpression);
if (next.HasValue)
{
Logger.LogInformation("Next at {next}", next);
}
}
else
{
var machineName = RedisManager.HashClient.GetOrSet(JobClientsCache, GetType().FullName, () => Environment.MachineName); // try get job master
if (machineName == Environment.MachineName) // IsMaster
{
using (var locker = RedisManager.GetRedLockClient($"{GetType().FullName}_cronService"))
{
// redis 互斥锁
if (await locker.TryLockAsync())
{
// 执行 job
await ProcessAsync(stoppingToken);
next = CronHelper.GetNextOccurrence(CronExpression);
if (next.HasValue)
{
Logger.LogInformation("Next at {next}", next);
await Task.Delay(next.Value - DateTimeOffset.UtcNow, stoppingToken);
}
}
else
{
Logger.LogInformation($"failed to acquire lock");
}
}
}
}
}
else
{
// needed for graceful shutdown for some reason.
// 1000ms so it doesn't affect calculating the next
// cron occurence (lowest possible: every second)
await Task.Delay(1000, stoppingToken);
}
}
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
RedisManager.HashClient.Remove(JobClientsCache, GetType().FullName); // unregister from jobClients
return base.StopAsync(cancellationToken);
}
}
Because the site is deployed on multiple machines, so in order to prevent concurrent execution, using redis done something, when Job executed attempt to get hostname redis in job corresponding master's, if not it is set to the current hostname of the machine, stop the job when the application is stopped, delete the current job redis corresponding master, when the job is executed to determine whether the master node is the master before the implementation of job, not the master is not performed. Complete implementation code: https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/CronScheduleServiceBase.cs#L11
Example Timing Job:
public class RemoveOverdueReservationService : CronScheduleServiceBase
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _configuration;
public RemoveOverdueReservationService(ILogger<RemoveOverdueReservationService> logger,
IServiceProvider serviceProvider, IConfiguration configuration) : base(logger)
{
_serviceProvider = serviceProvider;
_configuration = configuration;
}
public override string CronExpression => _configuration.GetAppSetting("RemoveOverdueReservationCron") ?? "0 0 18 * * ?";
protected override bool ConcurrentAllowed => false;
protected override async Task ProcessAsync(CancellationToken cancellationToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var reservationRepo = scope.ServiceProvider.GetRequiredService<IEFRepository<ReservationDbContext, Reservation>>();
await reservationRepo.DeleteAsync(reservation => reservation.ReservationStatus == 0 && (reservation.ReservationForDate < DateTime.Today.AddDays(-3)));
}
}
}
Complete implementation code: https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/RemoveOverdueReservationService.cs
Memo
Use redis this way to determine the master is not particularly reliable, ends normally without any problems, it is best with more mature registered service discovery framework is better