В работе регулярно встречаются задачи, которые необходимо выполнять с определенной периодичностью. Например, обновление раз в час цен в интернет-магазине. Для этого в cron кладется задание на выполнение скрипта по расписанию. Может так случиться (а случается это довольно часто), что скрипт не успевает выполниться за отведенное время. Через час запускается еще одна такая же задача. Два скрипта работают параллельно. Спустя еще час запускается третий скрипт. Нагрузка на сервер возрастает, потребление ресурсов растет как снежный ком. Избежать такой ситуации помогает проставление неких маркёров или флагов, которые показывают, что процесс еще запущен, и второй экземпляр запускать не надо. Существует 2 хороших и 2 так себе варианта решения этой проблемы.
Плохие варианты это хранить флаг в оперативке (кэше) или базе данных. Если кэш сбросят или перезагрузят кэширующий сервер, ваш флаг пропадет и привет дублирующий процесс. Если же ваш флаг находится в базе и случился какой-то сбой, например перезагрузка сервера, то новый скрипт вообще не запустится, т.к. у вас в базе стоит флаг, что процесс запущен. Придется лезть в табличку и обновлять значение. Оба эти варианты еще плохи тем, что в случае необработанной ошибке в скрипте, вас ждут те же проблемы, что написал выше.
Правильные же варианты выглядят так. Это либо делать портируемую консультативную блокировку файла, либо записывать в файл id запущенного процесса и при попытке запуска нового скрипта проверять процесса по id из файла (posix_kill(<pid из файла>, 0)).
Ниже представлен первый вариант решения проблемы дублирующихся скриптов:
$fp = fopen("/tmp/update_price.txt", "r+");
// либо "w+" для ограничения доступа к файлу, т.к. он будет открыт в режиме записи
if (flock($fp, LOCK_EX)) { // выполняем эксклюзивную блокировку
// либо flock($fp, LOCK_EX | LOCK_NB) для ускорения процесса блокировки
/* Этот кусок можно пропустить. Не обязательно что-то записывать,
* что бы осуществить блокировку.
* Но если надо записать какую-то техническую инфу, то можно
* использовать для этого создаваемый файл блокировки
* */
ftruncate($fp, 0); // очищаем файл
fwrite($fp, "Что-нибудь пишем сюда\n");
/** Конец необязательного кода **/
// выполняем какой-то полезный код, вызываем какую-нибудь функцию
fflush($fp); // очищаем вывод перед отменой блокировки (если в файл ничего не писали, то можно пропустить)
flock($fp, LOCK_UN); // отпираем файл (снимаем блокировку)
} else {
echo "Не удалось получить блокировку !";
}
// открываемый ранее файл надо закрыть
fclose($fp);
Второй вариант чуточку надежнее, т.к. запрашивает у сервера запущен ли проверяемый процесс или нет. В первом варианте при какой-либо ошибке и "падении" процесса блокировка снимается, но все равно есть нюансы и на 100% расчитывать нельзя. Зато в варианте с блокировкой можно задать ее тип и тогда второй запущенный скрипт либо сразу отваливается, либо ждет выполнения предыдущего (проверяется наличие флага LOCK_NB). Такую же реализацию можно сделать и через posix_kill, но это чуточку сложнее, чем с flock.
Вариант скрипта через posix_kill (скрипт большой, пришлось выложить его в gist.github): ссылка
Надеюсь, эта заметка была полезна и сподвигла вас писать более грамотный код.