Pull to refresh

Как запускать cron чаще, чем раз в минуту при помощи PHP

Reading time4 min
Views12K
Классический конфиг — файл с записями cron jobs в операционной системе Linux, выглядит следующим образом:

# ┌───────────── минуты (0 - 59)
# │ ┌───────────── часы (0 - 23)
# │ │ ┌───────────── день (1 - 31)
# │ │ │ ┌───────────── месяц (1 - 12)
# │ │ │ │ ┌───────────── день недели (0 - 6)
# │ │ │ │ │
# * * * * * <задача или команда>

Первые пять параметров означают время выполнения этой задачи, а шестой — сама команда, которую необходимо запустить. Параметры времени — это: минуты, часы, дни, месяцы и день недели. Причем все числа должны быть представлены в виде целых чисел либо в виде специального синтаксиса.

В итоге, минимально возможный интервал запуска команды — это один раз в минуту.

Для многих задач выполнение команды нужно намного чаще, например раз в 10 секунд. Для некоторых задач по автоматизации бизнес процессов максимально допустимая задержка часто составляет не более чем 1-1.5 секунды.

Разумеется, классический cron для этого не подходит — его нужно усовершенствовать.

Ниже представлена пошаговая реализация по созданию дополнительного функционала (на языке PHP) к классическому cron на Linux с применением дополнительной защиты от повторного запуска процессов.

Постановка задачи и настройка cron


Для примера будем использовать следующую задачу:

  1. Во фронт-энде пользователь может инициировать выполнение какой-то сложной задачи путем нажатия кнопки «Запуск»;
  2. Бек-энд после записи новой строки в базу данных сообщает пользователю подтверждение;
  3. Через cron мы будем «отслеживать» такие новые задачи и выполнять их максимально быстро, чтобы пользователь получил результат не через минуту, а моментально*.

*Если использовать запуск команд, как раз в минуту, то выполнение задачи начнется тогда, когда секунды дойдут до ближайшего нуля (начало новой минуты). Следовательно, в классическом виде пользователю нужно будет ожидать выполнение от 0 до 59 секунд.

Итак, cron настроим в его максимальном виде, т.е. раз в минуту:

* * * * * /usr/bin/php -q /run.php > /dev/null 2>&1
#/usr/bin/php - путь до установленного PHP на сервере (может отличаться в зависимости от версии ОС)
#/run.php - путь к PHP файлу на сервере
#> /dev/null 2>&1 - означает, что мы не будем записывать или логировать выходящую информацию из файла run.php

Выполнение одного цикла


Изначально следует определиться, с какой частотой мы будем запрашивать новые задачи в базе данных — в зависимости от этого будет меняться количество циклов и логический сон (функция sleep)

В текущем примере используется шаг равный 10 секундам. Следовательно количество циклов 60 / 10 = 6. Итого, общий код выглядит следующим образом:

for ($cycle = 1; $cycle <= 6; $cycle++) { #запускаем 6 циклов
    $all_tasks = get_all_tasks(); #получаем список задач из базы данных
    if ($all_tasks) { #если новые задачи найдены - продолжаем
        foreach($all_tasks as $one_task) { #итератор по каждой из полученных задач
            solve_one_task($one_task); #выполнение одной задачи
        }
    }
    sleep(10); #остановка выполнения скрипта, или "сон" на 10 секунд
}

Уточнение: в данном примере используется шаг равный 10 секундам, который может обеспечить минимальный интервал выполнения скрипта один раз в 10 секунд. Соответственно, для более частого выполнения следует изменить количество циклов и «время на сон».

Как избежать повторного выполнения задачи


В представленном виде есть одна неопределенность, а именно — повторное выполнение задачи в случае, если она уже начата. Это становится особенно актуально, если задача «сложная» и требует несколько секунд на ее реализацию.

Таким образом создается проблема повторного выполнения:

  • Функция solve_one_task() уже запущена, но еще не завершила свою работу;
  • Следовательно, в базе данных до сих пор задача отмечена как невыполненная;
  • Следующий цикл опять получит эту задачу и запустит функцию solve_one_task() еще раз, с этой же самой задачей.

Разумеется, это можно решить, например, изменением какого-то статуса в базе данных по этой задаче.

Но мы не будем нагружать базу данных: исходя из моего тестирования, MYSQL может принять запрос, но обработать его не сразу. Различие даже в 0.5 секунд может привести к повторному выполнению — что категорически не подходит.

Также в данном случае речь идет всего лишь про статусы задач, поэтому лучше будем использовать файловую систему сервера.

Основная модель проверки строится при помощи flock — функции, которая ставит и снимает блокировку с файла.

В исполнении PHP работу функции можно представить следующим образом:

$lock_file_abs = 'file'; #путь к файлу
$fp = fopen($lock_file_abs,"w+"); #открываем возможность чтения файла
if (flock($fp, LOCK_EX | LOCK_NB)) { #проверка, не заблокирован ли файл
    solve_one_task($one_task); #функция обработки задачи
    flock($fp, LOCK_UN); #снимаем блокировку с файла, потому что обработка задачи завершена
}
else {
    #значит, что файл заблокирован, т.е. на данный момент задача все еще обрабатывается
}
fclose($fp); #закрываем возможность чтения файла
unlink($lock_file_abs); #удаляем файл, если это возможно

Результат


Общий вид всего цикла выглядит следующим образом:

for ($cycle = 1; $cycle <= 6; $cycle++) {
    $all_tasks = get_all_tasks();
    if ($all_tasks) {
        foreach($all_tasks as $one_task) {
            $lock_file_abs = __DIR__.'/locks/run_'.$one_task['id'];
            $fp = fopen($lock_file_abs,"w+");
            if (flock($fp, LOCK_EX | LOCK_NB)) {
                solve_one_task($one_task);
                flock($fp, LOCK_UN);
            }
            else {
                #не можем запускать обработку задачи
            }
            fclose($fp);
            unlink($lock_file_abs);
        }
    }
    sleep(10);
}

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

И теперь достаточно просто запускать этот код через cron каждую минуту, а он в свою очередь, будет запускать более мелкие циклы уже внутри себя.
Tags:
Hubs:
Total votes 28: ↑4 and ↓24-20
Comments25

Articles