Pull to refresh

Блокировка дубликатов Symfony Сommand

Reading time3 min
Views5.4K
image

Сегодня хочу предложить вашему вниманию частный случай для решения «неудобств», связанных с периодичным запуском процессов в том случае, если предыдущий еще не завершился. Иначе говоря — блокировка запущенных процессов в symfony/console. Но все было бы слишком банально, если бы не необходимость блокировки среди группы серверов, а не на отдельно взятом.

Дано: Один и тот же процесс, который запускается на N серверов.
Задача: Сделать так, чтобы в единицу времени был запущен только один.

Наиболее популярные решения, которые можно встретить на «просторах»:

  1. блокировка через базу данных;
  2. сторонние приложения;
  3. нативное использование 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-файлами.

Спасибо за внимание!
Tags:
Hubs:
Total votes 11: ↑9 and ↓2+7
Comments28

Articles