• Час читання ~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 року, це законно).

Сигнали?

Так, сигнали. Тип Linuxy річ Джулія Еванс чудово викладає.

Сигнали - це події, які процес може слухати і вибирати відповідь. Наприклад, натискання ctrl+c на термінал надсилає SIGINT (перериває) поточний процес. Зазвичай це призводить до зупинки процесу.

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

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

Відправка SIGINT SIGKILL (kill -9 <process-id> або kill -s KILL <process-id>) особлива - вона негайно вб'є процес. Процес не має вибору в цьому питанні.

Давайте подивимося, як Laravel виконує витончені перезавантаження черги, і як ми можемо використовувати цю ідею в нашому власному коді.

Laravel Queues

Бібліотека черг Laravel реалізує сигнали, щоб граціозно зупинити працівників. Коли сигнал або SIGTERM SIGQUIT отримано, працівник чекає, поки поточна робота обробки закінчиться, перш ніж фактично зупинитися.

Тому робота не переривається в середині обробки - вона має шанс закінчити.

Це робиться за допомогою простої булевої змінної. Працівник - це, по суті, просто while() {} петля. Кожна ітерація перевіряє цю змінну і зупиняється, якщо $shouldQuit == true.

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

Чи не занадто чарівно!

SIGINT

Припинення () і вихід () мають явні назви - вони хочуть, щоб процес закінчився (SIGQUITSIGTERMрізниця полягає в тому, що SIGQUIT генерує дамп ядра).

А як щодо SIGINT (переривання)?

Це сигнал, що надсилається через ctrl+c. Зазвичай він використовується лише в інтерактивному терміналі - коли ми знаходимося за клавіатурою (для локальної розробки або для тих 1-вимкнених завдань у виробництві, які ви дійсно повинні автоматизувати).

Ви помітите, що SIGINT цього не слухають у черзі працівника Ларавеля! Натомість це обробляється 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).

У супервайзері тайм-аут встановлюється параметрами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() дзвінки, тому, якщо ваш код покладається на це, це може бути проблемою!

Навіщо це робити?

Корисним шаблоном для цього є виконання певної роботи з очищення перед виходом:Тепер ви можете запустити код очищення або методомhandleSignal(),

# 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;
    }
}

або після циклу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