In the process of evolution of a more or less large project, a situation may come when the number of scheduled tasks (cron jobs) becomes so large that their support becomes a nightmare for devops. To solve this problem, I had the idea to create an implementation of the scheduler in PHP, thereby making it part of the project, and the tasks themselves - part of its configuration. In this case, the necessary and sufficient number of cron jobs will be equal to one.
Some time ago I happened to develop a module for event planning. Some sort of simplified Google / Apple Calendar for users of the application. For storing dates and rules for repeating events, it was decided to use the iCalendar format ( RFC 5545 ), which allows one line to describe the schedule for repeating an event, taking into account the days of the week, months, the number of repetitions and much more. A few examples:
FREQ=WEEKLY;BYDAY=SU,WE
- Weekly on Saturday and WednesdayFREQ=MONTHLY;COUNT=5
- Every month, five timesFREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU
- Every second year on every Saturday of January
As you can see, this standard allows you to describe the rules for repeating an event much more flexibly than cron suggests.
A great library was found for working with the iCalendar format (do not take pity on a star):
https://github.com/simshaun/recurr
Having a tool for working with RRULE (Recurrence Rule), the matter remained for small. Write a few classes that allow you to plan and run tasks (which are any manifestation of PHP callable
type).
composer require hutnikau/job-scheduler
\Scheduler\Job\Job
- Class representing the task
To create an instance of it, you need a rule for its repetition (RRULE) and an instance of the type callable
:
$startTime = new \DateTime('2017-12-12 20:00:00'); $rule = new \Scheduler\Job\RRule('FREQ=MONTHLY;COUNT=5', $startTime); //run monthly, at 20:00:00 starting from the 12th of December 2017, 5 times $job = new \Scheduler\Job\Job($rule, function () { //do something });
Alternatively, use \Scheduler\Job\Job::createFromString()
:
$job = \Scheduler\Job\Job::createFromString( 'FREQ=MONTHLY;COUNT=5', //Recurrence rule '2017-12-28T21:00:00', //Start date function() {}, //Callback 'Europe/Minsk' //Timezone. If $timezone is omitted, the current timezone will be used );
Do not forget about time zones. I strongly advise you to always indicate them explicitly (not only when working with this library, but with \DateTime
in general) in order to avoid unpleasant surprises.
Add a task to the scheduler:
$scheduler = new \Scheduler\Scheduler() $scheduler->addJob($job);
You can also pass an array of tasks to the constructor:
$scheduler = new \Scheduler\Scheduler([ $job, //more jobs here ])
Run scheduled tasks:
$jobRunner = new \Scheduler\JobRunner\JobRunner(); $from = new \DateTime('2017-12-12 20:00:00'); $to = new \DateTime('2017-12-12 20:10:00'); $reports = $jobRunner->run($scheduler, $from, $to, true);
In this example, all tasks scheduled for the specified time period (10 minutes) will be performed. Thus, you need only one cron job that runs JobRunner
.
You can omit the $to
parameter, so that all tasks will be performed, ranging from $from
to the current moment.
The last parameter determines whether tasks that have been executed exactly on the boundary values ('2017-12-12 20:00:00' and '2017-12-12 20:10:00' from the example above) will be executed.
When you start the scheduler using cron, I advise you to save the time of the last launch, and the next time you start it, transfer it to the $from
parameter by adding one second, since the accuracy of cron is not perfect, and it is possible to skip any tasks or perform them twice.
$jobRunner->run(...)
returns an array of results of completed tasks (an array of objects of type \Scheduler\Action\Report
).
\Scheduler\Action\Report { /* Methods */ public mixed getReport ( void ) public Action getAction ( void ) public string getType ( void ) }
By calling \Scheduler\Action\Report::getReport()
you can get the result of running callable (the value returned by it).
In case an exception was thrown during the execution of the task, \Scheduler\Action\Report::getReport()
will return the exception itself.
The \Scheduler\Action\Report::getAction()
will return an instance of the \Scheduler\Action\ActionInterface
that describes the action taken. Using it, you can find out the time to complete the action or get the action itself (Job).
It is also worth noting that if the scheduled task was to be executed more than once (for example, if the MINUTELY interval was used in MINUTELY
, and the difference between $from
and $to
, transferred to JobRunner
10 minutes), then the action will be executed several times. In other words, they will not be grouped.
Here, perhaps, that's all. The library is really small, but I hope it will be useful to someone.
Constructive criticism and developmental assistance are welcome.
Source: https://habr.com/ru/post/345802/
All Articles