Pull to refresh

Простой планировщик задач на PHP

Reading time 3 min
Views 30K
image

В процессе эволюции более-менее крупного проекта может настать ситуация, когда количество запланированных задач (cron jobs) становится настолько большим, что поддержка их становится ночным кошмаром devops'ов. Для решения этой проблемы мне пришла в голову идея создать реализацию планировщика на PHP, тем самым сделав его частью проекта, а сами задачи — частью его конфигурации. В этом случае необходимое и достаточное количество cron jobs будет равно единице.


Некоторое время назад мне довелось разрабатывать модуль для планирования событий. Некое упрощенное подобие Google/Apple Calendar для пользователей приложения. Для хранения дат и правил повторения событий было решено использовать формат iCalendar (RFC 5545), позволяющий одной строкой описать график повторения какого-либо события с учетом дней недели, месяцев, количества повторений и многого другого. Несколько примеров:


FREQ=WEEKLY;BYDAY=SU,WE — Еженедельно в субботу и среду
FREQ=MONTHLY;COUNT=5 — Каждый месяц, пять раз
FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU — Каждый второй год в каждую субботу января


Как видите данный стандарт позволяет описать правила повторения события гораздо более гибко, чем предлагает cron.


Для работы с форматом iCalendar была найдена замечательная библиотека (не пожалейте звезду):
https://github.com/simshaun/recurr


Имея инструмент для работы с RRULE (Recurrence Rule) дело осталось за малым. Написать несколько классов, позволяющих планировать и запускать задачи (являющиеся каким угодно проявлением PHP callable типа).


Установка библиотеки:


composer require hutnikau/job-scheduler


Планирование и запуск задач:


\Scheduler\Job\Job — Класс, представляющий задачу


Для создания его экземпляра потребуется правило его повторения (RRULE) и экземпляр типа 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
});

Альтернативный вариант — использовать \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
);

Не забывайте о временных зонах. Настоятельно советую всегда указывать их явно (не только при работе с этой библиотекой, а с \DateTime в целом) во избежание неприятных сюрпризов.


Добавляем задачу в планировщик:


$scheduler = new \Scheduler\Scheduler()
$scheduler->addJob($job);

Так же можно передать массив задач в конструктор:


$scheduler = new \Scheduler\Scheduler([
    $job,
    //more jobs here
])

Запускаем запланированные задачи:


$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);

В данном примере будут выполнены все задачи, запланированные на указанный промежуток времени (10 минут). Таким образом вам потребуется всего один cron job, запускающий JobRunner.


Можно опустить параметр $to, таким образом будут выполнены все задачи, начиная от $from до текущего момента.


Последний параметр определяет, будут ли выполнены задачи, время выполнения которых попало точно на пограничные значения ('2017-12-12 20:00:00' и '2017-12-12 20:10:00' из примера выше).


При запуске планировщика при помощи cron я советую сохранять время последнего запуска, и при следующем запуске передавать его в параметр $from прибавив одну секунду, так как точность cron'а не идеальна, и существует вероятность пропустить какие-либо задачи или выполнить их дважды.


$jobRunner->run(...)возвращает массив результатов выполненных задач (массив объектов типа \Scheduler\Action\Report).


\Scheduler\Action\Report {
    /* Methods */
    public mixed getReport ( void )
    public Action getAction ( void )
    public string getType ( void )
}

Вызвав \Scheduler\Action\Report::getReport() можно получить результат выполнения callable (возвращенное им значение).


В случае, если при выполнении задачи было брошено исключение, \Scheduler\Action\Report::getReport() вернет то самое исключение.


Метод \Scheduler\Action\Report::getAction() вернет экземпляр типа \Scheduler\Action\ActionInterface, который описывает выполненное действие. Используя его можно узнать время выполнения действия или получить само действие (Job).


Так же стоит обратить внимание, что если запланированная задача должна была выполниться более одного раза (например если в RRULE был использован интервал MINUTELY, и разница между $from и $to, переданным в JobRunner 10 минут), то действие будет выполнено несколько раз. Другими словами они не будут сгруппированы.


Вот, пожалуй, и все. Библиотека действительно мала, но надеюсь окажется кому-либо полезной.
Конструктивная критика и помощь в развитии приветствуются.


GitHub
packagist

Tags:
Hubs:
+22
Comments 32
Comments Comments 32

Articles