Сегодня хочу предложить вашему вниманию частный случай для решения «неудобств», связанных с периодичным запуском процессов в том случае, если предыдущий еще не завершился. Иначе говоря — блокировка запущенных процессов в symfony/console. Но все было бы слишком банально, если бы не необходимость блокировки среди группы серверов, а не на отдельно взятом.
Дано: Один и тот же процесс, который запускается на N серверов.
Задача: Сделать так, чтобы в единицу времени был запущен только один.
Наиболее популярные решения, которые можно встретить на «просторах»:
- блокировка через базу данных;
- сторонние приложения;
- нативное использование lock-файла
Основные минусы каждого из них:
База данных
- требует подключение к базе в каждом запускаемом скрипте;
- нужна таблица;
- нужен код, обслуживающий запись/удаление;
- сложности при «падении» скрипта с тем, как снять lock, нужен watchDog;
- сложности при «падении» самой базы
Сторонние приложения (к примеру, run-one для Ubuntu)
- не для всех платформ есть одинаковые приложения с одинаково предсказуемым поведением;
- не всегда есть возможность установить что-то дополнительное;
- не все умеют блокировать «в сети»
Нативные lock-файлы
- каждая команда должна сопровождаться созданием файла;
- сколько команд — столько строк с путем и именем lock-файла
Наиболее распространенный, конечно же, — 3й вариант, но он создает очень много неудобств при наличии большого кол-ва серверов и процессов. Поэтому я решил поделиться идеей написания singleton-команды на базе symfony/console. Но идею можно использовать и в любом другом фреймворке.
Итак, первое же, от чего пришлось отказаться — flock, который используется, к примеру, в LockHandler от symfony. Он не дает возможность блокировки среди нескольких серверов.
Вместо этого будем создавать lock-файл в расшаренной между серверами директории, с помощью маленького сервиса, это практически аналог LockHandler, но с «выпиленным» flock.
Следующее, от чего нужно избавиться — необходимость в каждой команде проверять вручную блокировку, и, самое главное — снимать ее, ведь не всегда скрипт завершается там, где мы предполагаем.
Для этого предлагаю применить нечто, похожее на Mediator — реализовать и финализировать стандартный метод execute(), который будет запущен при старте команды и навязать использование нового метода lockExecute().
Для чего это нужно:
- весь код команды будет содержаться в методе lockExecute();
- вызываемый при запуске метод execute() будет создавать блокировку, регистрировать снятие блокировки при падении/завершении скрипта и только потом — выполнять lockExecute()
В итоге, стандартная команда symfony:
class CreateUserCommand extends Command
{
protected function configure()
{
// ...
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// ...
}
}
будет выглядеть так:
class CreateUserCommand extends SingletonCommand implements SingletonCommandInterface
{
protected function configure()
{
// ...
}
public function lockExecute(InputInterface $input, OutputInterface $output)
{
// ...
}
}
Писать значительно больше кода не придется и при этом она будет гарантированно запущена только 1 раз, сколько бы серверов не попытались это сделать. Единственное условие — общая директория для lock-файлов.
Уже готовое решение и больше деталей можно посмотреть на гитхаб: singleton-command
UPD: как справедливо было замечено — в случае «жестких» падений скриптов, возможно сохранение lock-файлов. Поэтому, желательно организовать демона, который будет «наблюдать» за «залежавшимися» lock-файлами.
Спасибо за внимание!