Запуск только одной копии скрипта

1st Февраль 2013 | Категории: PHP | Метки: , ,

Есть консольный скрипт. Скрипт получает набор данных, обрабатывает, записывает результат в БД, отмечает статус выполнения. Если этот скрипт запустить с небольшим интервалом два и более раз, то возможна ситуация, при которой запущенные в разное время скрипты могут обрабатывать одни и те же данные.

Нечто подобное происходило в моей системе. По крону был назначен запуск скрипта с интервалом в 5 минут. Скрипт получал список из 300 URL, скачивал страницы и обрабатывал их. В случае недоступности удаленного хоста, глюков самого скрипта или солнечных бурь, происходило «наступание на хвост». В итоге я уменьшил таймаут ожидания скачивания страницы до 5 секунд, а количество выдаваемых URL до 100. В итоге за час стало обрабатываться не более 1200 страниц.

Но ведь проблему можно решить и по-другому. Задача ведь проста – не дать запуститься двум копиям скрипта. И сделаем мы это с помощью системы блокировок временного файла.

Алгоритм:

  • Проверяем, заблокирован ли файл по указанному адресу
  • Если файл заблокирован – die
  • Иначе – записываем в него полезную информацию (например, текущую метку времени)
  • Выполняем необходимые операции
  • При завершении скрипта – закрываем файл

В итоге при запуске скрипта, если запущена единственная копия, он будет работать как обычно. Если одновременно запустить еще одну копию, он сразу же завершит свою работу.
Код примера:

<?php
$begin_time = time();
$lock_file = __DIR__.'/lock_test';
$fp = fopen($lock_file, "w+");
 
// регистрируем функцию shutdown, которая будет выполнена при завершении скрипта
register_shutdown_function('shutdown', &$begin_time, &$fp);
 
// проверяем статус блокировки
if (!flock($fp, LOCK_EX | LOCK_NB)) {
    die("another script running");
}
 
fwrite($fp, date('Y-m-d H:i:s'));
echo "running" . PHP_EOL;
 
//бесконечный цикл
while (1 == 1) {
	// выполняем полезные действия
	process();
	// ограничиваем выполнение одной минутой
	if (time() >= $begin_time+60) {
		die;
	}
}
 
// для примера - просто засыпаем на 1 секунду
function process()
{
	sleep(1);
}
 
// в экстренном случае мы должны закрыть файл
function shutdown($begin_time, $fp)
{
	echo "total time: " . ( time()-$begin_time ) . PHP_EOL;
	fclose($fp);
}

Пример запуска. Открываем две консоли. В первой консоли (после запуска скрипт работает 60 секунд):

# php test.php
running
total time: 60

Во второй консоли (пока не прошли 60 секунд):
# php test.php
another script running total time: 0
# php test.php
another script running total time: 0
# php test.php
running

Поясню. В первой консоли мы запустили скрипт, который успешно начал выполнять свои задачи. Сразу после этого переключаемся на вторую консоль и пробуем запустить там тот же самый скрипт. В итоге скрипт моментально завершает работу (время выполнения 0 секунд). Ждем минуту и пробуем во второй консоли запустить тот же самый скрипт (вдруг баг консоли или еще что-то не предвиденное). И видим сообщение о том, что скрипт нормально запустился. Задача выполнена.

Чуть подробнее о моем реальном примере. Скрипт, о котором я говорил в начале статьи, успешно переписан. За основу взят пример из статьи, с той лишь разницей, что ограничение на выполнение увеличено до 59 минут ($begin_time+60*59). Скрипт непрерывно опрашивает базу данных, получает задачи и выполняет их. Отработав 59 минут он завершается. Cron настроен на ежеминутное выполнение этого скрипта. В итоге:
– Если все работает без ошибок: скрипт отработает 59 минут, обработает за это время до 3540 страниц и успешно завершится. Все эти 59 минут другие копии не смогут запускаться.
– Если скрипт умирает с ошибкой: происходит остановка в момент ошибки. В следующую минуту новая копия скрипта запустится и будет пытаться выполнять задачи.

Подобная схема позволяет нам не беспокоиться об утечках памяти, возможных крахах скрипта и прочих форс-мажорных ситуациях.

Subscribe without commenting


  1. 2nd Февраль 2013 в 20:20

    Можно хитрее, организовать очередь через СУБД.
    Параллельно работает несколько скриптов(неограниченное количество), конфликтов доступа нет. алгоритм:
    для очереди в реляционной субд(нужно придумать уникальный id для каждого запуска скрипта):
    update queue set owner = id limit 1;
    select * from queue where owner = id;

  2. Тарлюн Максим
    2nd Февраль 2013 в 20:34

    @IAD
    Все зависит от задачи.
    К примеру надо мониторить удаленные ресурсы. А для этого скачивать состояние с определенной страницы. Тогда физически нельзя допускать одновременной работы двух скриптов.

    Один раз я делал примерно как описали вы. Задача была — скачать 1200000 страниц с ограничением в 1сек/запрос с одного IP. Что бы ускорить процесс — запустил в 10 потоков, каждый через свою прокси. Задачи распределил между ними, исходя из остатка от деления на 10.
    То есть все id с последней цифрой 1 — получил первый скрипт.
    С окончанием 2 — второй скрипт. И так далее.

  3. pumbo
    9th Март 2013 в 10:43

    Зачем использовать register_shutdown_function, если lock-файл в любом случае автоматически закроется при завершении скрипта?

  4. Тарлюн Максим
    9th Март 2013 в 11:00

    @pumbo
    Спасибо, не знал.
    К тому же привык действовать надежно: die в конце скриптов, очистка переменных после использования, принудительное закрытие файла и так далее.

  5. 10th Декабрь 2014 в 11:08

    Спасибо за информацию, вроде рабочий метод!