# -------------- минуты (0 - 59)
# ¦ -------------- часы (0 - 23)
# ¦ ¦ -------------- день (1 - 31)
# ¦ ¦ ¦ -------------- месяц (1 - 12)
# ¦ ¦ ¦ ¦ -------------- день недели (0 - 6)
# ¦ ¦ ¦ ¦ ¦
# * * * * * <задача или команда>
Первые пять параметров означают время выполнения этой задачи, а шестой — сама команда, которую необходимо запустить. Параметры времени — это: минуты, часы, дни, месяцы и день недели. Причем все числа должны быть представлены в виде целых чисел либо в виде специального синтаксиса.
В итоге, минимально возможный интервал запуска команды — это один раз в минуту.
Для многих задач выполнение команды нужно намного чаще, например раз в 10 секунд. Для некоторых задач по автоматизации бизнес процессов максимально допустимая задержка часто составляет не более чем 1-1.5 секунды.
Разумеется, классический cron для этого не подходит — его нужно усовершенствовать.
Ниже представлена пошаговая реализация по созданию дополнительного функционала (на языке PHP) к классическому cron на Linux с применением дополнительной защиты от повторного запуска процессов.
Постановка задачи и настройка cron
Для примера будем использовать следующую задачу:
- Во фронт-энде пользователь может инициировать выполнение какой-то сложной задачи путем нажатия кнопки «Запуск»;
- Бек-энд после записи новой строки в базу данных сообщает пользователю подтверждение;
- Через 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 каждую минуту, а он в свою очередь, будет запускать более мелкие циклы уже внутри себя.
ghrb
Использовать для этого другой инструмент.
RomanRuzin Автор
Разумеется, как и в математике и во многих других сферах — есть множество решений как решить задачу. В данной статье я описал один из возможных вариантов. Буду рад, если вы поделитесь описанием «другого инструмента».
ar2rsoft
Как вариант, можно заменить цикл на бесконечный и запустить это через supervisord
gluck59
Это вряд ли.
DSolodukhin
Например, systemd-timers.