• Время чтения ~7 мин
  • 12.07.2023
the graceful act of cleaning up after yourself
Image by Annie Ruygt

Fly.io можете создавать и запускать свое приложение Laravel по всему миру. Разверните свое приложение Laravel на Fly.io, и вы будете готовы к работе за считанные минуты!

Очереди Laravel останавливаются изящно. Что это значит?

Во время развертывания вы, скорее всего, перезапустите рабочие процессы очереди, используя что-то вроде artisan queue:restart или supervisorctl restart <worker-name>.

Ларавел любезно заметил, что нам не нравится, когда незавершенная работа внезапно убивается. Что делает Laravel, так это проверяет, должен ли работник очереди остановиться. Если это необходимо, работник ожидает завершения текущего выполняемого задания, а затем завершает работу.

Он делает это с помощью сигналов. (Все мы знаем, что если страница использует форматирование HTML по умолчанию 1990-х годов, это законно).

Сигналы?

Да, сигналы. Джулия Эванс отлично преподает.

Сигналы — это события, которые процесс может прослушивать и на которые можно реагировать. Например, нажатие ctrl+c на ваш терминал отправляет SIGINT (прерывание) текущему запущенному процессу. Обычно это приводит к остановке процесса.

Вы также можете использовать kill команду для отправки сигнала (любого сигнала). Отправка через kill выглядит так:

# These are equivalent
kill -2 <process-id>`
kill -s INT <process-id>

Отправка SIGKILL SIGINT (kill -9 <process-id> или kill -s KILL <process-id>) особенная - она сразу же убьет процесс. У процесса нет выбора в этом вопросе.

Давайте посмотрим, как Laravel выполняет изящные перезапуски очереди, и как мы можем использовать эту идею в нашем собственном коде.

Laravel Queues

Библиотека очередей Laravel реализует сигналы, чтобы изящно останавливать работников. При получении сигнала «илиSIGQUIT» SIGTERM работник ожидает завершения текущего задания обработки, прежде чем фактически остановиться.

Поэтому работа не прерывается в середине обработки - у нее есть шанс закончить.

Это делается с помощью простой логической переменной. Работник - это, по сути, просто цикл.while() {} На каждой итерации он проверяет эту переменную и останавливается, если $shouldQuit == true.

Мы видим, что Laravel слушает и то, и другое и SIGTERM SIGQUIT сигнализирует здесь. Прослушивание этих сигналов настраивается непосредственно перед запуском вышеупомянутого while() цикла.

Не слишком волшебно!

SIGINT

Термин () и выход () имеют очевидное название - они хотят, чтобы процесс завершился (SIGTERMSIGQUITразница в том, что SIGQUIT он генерирует дамп ядра).

А как насчет SIGINT (прервать)?

Это сигнал, отправленный через ctrl+c. Обычно он используется только в интерактивном терминале - когда мы находимся за клавиатурой (для локальной разработки или для тех 1-разовых задач в продакшене, которые вы действительно должны автоматизировать).

Вы заметите, что это SIGINT не прослушивается в воркере очереди Laravel! Вместо этого это обрабатывается PHP, и он просто прекращает работу всего, что происходит. Следовательно, это не способ изящно выйти из процесса!

Давайте посмотрим на это быстро. Я создал задание с именемLongJob, которое просто спит в течение 10 секунд:

<?php
namespace App\Jobs;
use ...
class LongJob implements ShouldQueue
{
    use ...
    public function handle(): void
    {
        Log::info("starting LongJob ".$this->job->getJobId());
        Sleep::for(10)->seconds();
        Log::info("finished LongJob ".$this->job->getJobId());
    }
}

У меня был открыт терминал, запущен php artisan queue:work. Затем я отправил эту работу и быстро ударил ctrl+c. Журналы показали, что работа началась, но так и не закончилась!

[2023-06-28 14:19:09] local.INFO: starting LongJob 1 

Если вместо этого я отправлю ему сигналSIGTERM, он завершит работу, а затем выйдет:

# Start a worker
php artisan queue:work
# Find the process ID
ps aux | grep queue:work
# Dispatch a job, and then 
# kill the  worker with SIGTERM
# Process ID 69679 in my case
kill -s TERM 69679

Мы увидим, что задание завершится до завершения процесса! Однако сон не спал 10 секунд. Подробнее об этом ниже!

[2023-06-28 14:19:09] local.INFO: starting LongJob 2  
[2023-06-28 14:19:11] local.INFO: finished LongJob 2  

Корректное перезапуск во время развертывания

Рабочий процесс рабочей очереди обычно отслеживается монитором процесса, таким как супервизор. Это перезапускает любой процесс, который останавливается неожиданным образом.

Рабочий процесс очереди Laravel использует это, останавливая рабочий процесс в нескольких случаях (условия ошибки, время artisan queue:restart запуска и т. д.), поскольку он может предположить, что рабочий процесс очереди перезапустится при необходимости.

Супервайзер и друзья обычно останавливают процесс, посылая сигнал, а затем ожидаяSIGTERM, пока процесс изящно выйдет сам (что Laravel сделает сам, как описано выше).

Как правило, супервайзер (или кто-то еще) дает процессу определенное количество секунд для завершения. Если это время истекает, то SIGKILL отправляется, и процесс принудительно останавливается (поскольку процессы не могут игнорировать SIGKILL).

В Supervisor тайм-аут задается параметрамиstopwaitsecs. В документации Laravel этот параметр установлен на один час (в секундах) в своем примере. Вы можете снизить это, если ваши задания не выполняются долгое время и вам никогда не понадобится час для завершения.

Использование сигналов в нашем коде

Давайте посмотрим, как реализовать сигналы самостоятельно!

Задания уже полностью завершены, когда поступает сигнал, чтобы остановить их. С точки зрения нас, разработчиков, мы, скорее всего, захотим улавливать сигналы из наших ремесленных команд.

Первое, что мы увидим, это то, как сигнал немедленно остановит процесс (выполняемую команду).

Обратите внимание, что я буду использовать здесь «процесс» и «команда» взаимозаменяемо. Выполнение команды, напримерphp artisan whatever, запускает процесс PHP. Этот процесс запускаетсяartisan, который загружает фреймворк, запускает нашу команду, yadda yadda yadda. Дело в том, что здесь работают оба слова!

Я создал команду LongCommand и заставил ее спать в течение 10 секунд (точно так же, как LongJob).

public function handle()
{
    $this->info('starting long command: '.now()->format('H:i:s'));
    Sleep::for(10)->seconds();
    $this->info('finished long command: '.now()->format('H:i:s'));
}

Если я запускаю это через artisan longtime и использую ctrl+c, он немедленно останавливается:

starting long command 14:27:59
^C%   

Не было выходных данных, показывающих, что он завершает команду!

Ловушка

Мы можем «заманить в ловушку» - прослушать - сигнал. Это позволяет нам запускать код до выхода команды.

Ловушка lets you capture a signal, and do something in response, but it will then exit immediately.

На самом деле это может быть ошибка, я не совсем уверен!

public function handle()
{
    $this->trap(
      [SIGINT, SIGTERM], 
      fn($s) => $this->info('signal received: ' . $s)
    );
    $this->info('starting long command: '.now()->format('H:i:s'));
    Sleep::for(30)->seconds();
    $this->info('finished long command: '.now()->format('H:i:s'));
}

Мы заманиваем в ловушку SIGINT и SIGTERM просто повторяем какую-то информацию. Это дает нам возможность запустить код очистки перед выходом!

starting long command: 14:29:50
^Csigint received   

Сигнал SIGINT был «пойман», но все равно остановил процесс! Мы получаем аналогичное поведение для SIGTERM:Таким

starting long command: 14:33:39
sigint received 

образом, мы можем реагировать на сигнал, но на самом деле мы не можем остановить завершение процесса после выполнения обратного вызова.

Было бы полезно, если бы мы могли игнорировать сигнал до тех пор, пока не будем готовы к выходу! К счастью, мы можем.

❤️ Fly.io Laravel

Размещайте свои серверы рядом с пользователями и удивляйтесь скорости близкого расположения. Выполняйте глобальное развертывание на Fly за считанные минуты!

Разверните приложение Laravel!  →

Реализация SignalableCommandInterface

Если наша команда реализует Symfony SignalableCommandInterface, мы можем заставить команду завершить выполнение до того, как она завершит работу.

(Вы можете сказать, что это вещь Symfony, потому что она не названа чем-то приятным, как, скажем, Signalable).

На первый взгляд кажется, что это работает так же, как и метод $this->trap() . Однако, если мы return false в нашем обработчике, команда может закончить свою работу.

Вот как это выглядит:

# Some stuff omitted
use Symfony\Component\Console\Command\SignalableCommandInterface;
class LongCommandTwo extends Command implements SignalableCommandInterface
{
    public function handle()
    {
        $this->info('starting long2 cmd: '.now()->format('H:i:s'));
        Sleep::for(30)->seconds();
        $this->info('finished long2 cmd: '.now()->format('H:i:s'));
    }
    public function getSubscribedSignals(): array
    {
        return [SIGINT, SIGTERM];
    }
    public function handleSignal(int $signal)
    {
        $this->info('signal received: ' . $signal);
        return false;
    }
}

Поскольку обработчик сигнала возвращает false, наша команда может завершить. То, как это работает, является лишь деталью реализации обработки SignalableCommandInterface Symfony - он говорит коду не запускаться exit($statusCode);.

После реализации этого мы можем увидеть наши «законченные...» строка запущена:

starting long2 cmd: 15:14:01
^Csignal received: 2
finished long2 cmd: 15:14:02

Вы могли заметить, что мы на самом деле не спали в течение 30 секунд! Это относится к использованию sleep() для тестирования. Использование сигналов на самом деле ярлыки для текущих вызовов sleep() , поэтому, если ваш код полагается на это, это может быть проблемой!

Зачем это делать?

Полезным шаблоном для этого является выполнение некоторой работы по очистке перед выходом:

# Some stuff omitted
use Symfony\Component\Console\Command\SignalableCommandInterface;
class LongCommandTwo extends Command implements SignalableCommandInterface
{
    protected $shouldExit = false;
    public function handle()
    {
        $this->info('starting long2 cmd: '.now()->format('H:i:s'));
        while(! $this->shouldExit) {
            $this->info("We're doing stuff");
            Sleep::for(1)->seconds();
        }
        // Pretend we're working hard on
        // cleaning everything up
        // Oh, also, this sleep actually happens
        // since it was started after the signal was received
        Sleep::for(10)->seconds();
        $this->info('finished long2 cmd: '.now()->format('H:i:s'));
    }
    public function getSubscribedSignals(): array
    {
        return [SIGINT, SIGTERM];
    }
    public function handleSignal(int $signal,)
    {
        $this->shouldExit = true;
        $this->info('Cleaning up: signal received: '.$signal);
        return false;
    }
}

Теперь вы можете запустить некоторый код очистки либо в методе, handleSignal() либо после цикла while() . Это отлично подходит для удаления временных файлов, обеспечения целостности данных при отключении команды, закрытия сетевых подключений и многого другого!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297