. net core implements task scheduling based on cron expression

Intro

Last time we implemented a simple Timer-based timing task. Details can be seen This article.

However, it may not be appropriate to use this method slowly. Some tasks may only want to execute in a certain period of time. It is not so flexible to use timer only. We hope that we can specify a cron expression like quartz to specify the execution time of tasks.

Introduction of cron expression

cron is common in Unix and Unix-like Of operating system For setting instructions that are periodically executed. This command reads instructions from standard input devices and stores them in a "crontab" file for later reading and execution. The word comes from the Greek word chronos (p_), which originally means time.

Usually, crontab stored instructions are activated by daemons, crond often runs in the background, checking every minute for scheduled jobs to be executed. Such jobs are generally called cron jobs.

Cron can accurately describe the execution time of periodic tasks. The standard cron expression is five bits:

30 4 * *? The values at the five locations correspond to minutes / hours / days / months / weeks, respectively.

Now there are some extensions. There are six bits and seven bits. The first expression of six bits corresponds to seconds, the first one corresponds to seconds, and the last one corresponds to years.

0 12 * *? 12 noon every day
0 1510?** 10:15 a day
0 1510 *? 10:15 a day
30 15 10 * *?* 10:15:30 a day
0 1510 * *? 10:15 a day in 2005

Detailed information can be consulted: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

.NET Core CRON service

CRON parsing libraries use https://github.com/HangfireIO/Cronos
Five or six people are supported, and the year analysis is not supported for the time being (seven people)

The RON timing service based on BackgroundService is 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 mutex
                                    if (await locker.TryLockAsync())
                                    {
                                        // Executing 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 website is deployed on multiple machines, in order to prevent concurrent execution, we use redis to do some things. When Job executes, we try to get the host name of the master corresponding to the job in redis. If not, we set the host name of the current machine. When job stops, we delete the re when the application stops. The master corresponding to the current job in dis determines whether the master node is the master node when the job is executed. It is the master that executes the job, but not the master that does not. Complete implementation code: https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/CronScheduleServiceBase.cs#L11

Timing Job example:

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

It's not particularly reliable to use redis to decide the master. There's nothing wrong with the normal end. It's better to use a more mature service registration and discovery framework.

Reference

Keywords: PHP github Redis crontab Unix

Added by Das Capitolin on Sun, 04 Aug 2019 18:46:26 +0300