Based .net core task scheduling cron expression

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 crontabinstructions are stored daemon activated, crondoften 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 * * ? 20052005 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 BackgroundServicethe 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

Reference

Guess you like

Origin www.cnblogs.com/weihanli/p/implement-job-schedule-via-cron-for-dotnetcore.html