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
Термин () и выход () имеют очевидное название - они хотят, чтобы процесс завершился (SIGTERM
SIGQUIT
разница в том, что 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
образом, мы можем реагировать на сигнал, но на самом деле мы не можем остановить завершение процесса после выполнения обратного вызова.
Было бы полезно, если бы мы могли игнорировать сигнал до тех пор, пока не будем готовы к выходу! К счастью, мы можем.
Реализация 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()
. Это отлично подходит для удаления временных файлов, обеспечения целостности данных при отключении команды, закрытия сетевых подключений и многого другого!