• Czas czytania ~7 min
  • 12.07.2023
the graceful act of cleaning up after yourself
Image by Annie Ruygt

Fly.io może tworzyć i uruchamiać aplikację Laravel na całym świecie. Wdróż swoją aplikację Laravel na Fly.io, a będziesz gotowy do pracy w ciągu kilku minut!

Kolejki Laravel są zatrzymywane z wdziękiem. Co to oznacza?

Podczas wdrażania prawdopodobnie ponownie uruchomisz procesy robocze kolejki za pomocą czegoś takiego jak artisan queue:restart lub supervisorctl restart <worker-name>.

Laravel łaskawie zauważył, że nie lubimy, gdy nagle ginie praca w trakcie procesu. To, co robi Laravel, to sprawdzanie, czy pracownik kolejki powinien się zatrzymać. Jeśli powinien, pracownik czeka, aż aktualnie uruchomione zadanie zostanie zakończone, a następnie wyjdzie.

Robi to za pomocą sygnałów. (Wszyscy wiemy, że jeśli strona używa domyślnego formatowania HTML z 1990 roku, jest to legalne).

Sygnały?

Tak, sygnały. Typ linuksowej rzeczy Julia Evans jest świetna w nauczaniu.

Sygnały to zdarzenia, których proces może nasłuchiwać i na które może reagować. Na przykład uderzenie ctrl+c w terminal wysyła SIGINT: (przerwanie) do aktualnie uruchomionego procesu. Zwykle powoduje to zatrzymanie procesu.

Możesz także użyć poleceniakill, aby wysłać sygnał (dowolny sygnał). Wysyłanie przez kill wygląda tak:

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

Wysyłanie SIGKILL SIGINT: (kill -9 <process-id> lub kill -s KILL <process-id>) jest specjalne - natychmiast zabije proces. Proces nie ma wyboru w tej sprawie.

Zobaczmy, jak Laravel wykonuje wdzięczne ponowne uruchamianie kolejki i jak możemy wykorzystać ten pomysł w naszym własnym kodzie.

Laravel Kolejki Biblioteka

kolejek Laravela implementuje sygnały, aby zatrzymać pracowników z wdziękiem. Po odebraniu sygnału lub SIGTERM SIGQUIT pracownik czeka na zakończenie bieżącego zadania przetwarzania, zanim faktycznie się zatrzyma.

Dlatego zadanie nie jest przerywane w trakcie przetwarzania - ma szansę zakończyć.

Odbywa się to za pomocą prostej zmiennej logicznej. Robotnik jest w zasadzie tylko pętląwhile() {}. Każda iteracja sprawdza tę zmienną i zatrzymuje się, jeśli $shouldQuit == true.

Widzimy, że Laravel nasłuchuje obu SIGTERM i SIGQUIT sygnalizuje tutaj. Nasłuchiwanie tych sygnałów jest ustawiane tuż przed uruchomieniem wspomnianej while() pętli.

Niezbyt magiczne!

SIGINT:

Zakończenie () i zakończenie () są oczywiście nazwane - chcą, aby proces się zakończył (SIGQUITSIGTERMróżnica polega na tym, że SIGQUIT generuje zrzut rdzenia).

What about SIGINT: (interrupt)?

Jest to sygnał wysyłany przez ctrl+c. Zazwyczaj jest używany tylko w interaktywnym terminalu - gdy jesteśmy przy naszej klawiaturze (dla lokalnego rozwoju lub dla tych 1-off zadań w produkcji, naprawdę powinieneś zautomatyzować).

You'll notice that SIGINT: isn't listened for in Laravel's queue worker! Instead, that's handled by PHP, and it just quits whatever happens to be running. It is, therefore, not a way to gracefully exit a process!

Zobaczmy to szybko. Stworzyłem zadanie o nazwieLongJob, które po prostu śpi przez 10 sekund:

<?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());
    }
}

miałem terminal otwarty, uruchomiony php artisan queue:work. Potem wysłałem tę pracę i szybko trafiłemctrl+c. Dzienniki pokazały, że zadanie rozpoczęło się, ale nigdy nie zostało zakończone!

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

Jeśli zamiast tego wyślę mu sygnałSIGTERM, zakończy pracę, a następnie wyjdzie:

# 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

Zobaczymy, że zadanie kończy się przed zakończeniem procesu! Jednak sen nie spał przez 10 sekund. Więcej na ten temat poniżej!

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

Bezpieczne ponowne uruchamianie podczas wdrażania

Proces roboczy kolejki produkcyjnej jest zwykle monitorowany przez monitor procesu, taki jak Supervisor. Spowoduje to ponowne uruchomienie każdego procesu, który zatrzyma się w nieoczekiwany sposób.

Pracownik kolejki Laravela wykorzystuje to, zatrzymując pracownika w kilku przypadkach (warunki błędu, kiedy artisan queue:restart jest uruchamiany itp.), ponieważ może założyć, że pracownik kolejki uruchomi się ponownie w razie potrzeby.

Przełożony i przyjaciele zwykle zatrzymują proces, wysyłając SIGTERM sygnał, a następnie czekając, aż proces z wdziękiem sam się wyjdzie (co Laravel zrobi sam, jak opisano powyżej).

Zazwyczaj nadzorca (lub cokolwiek innego) daje procesowi określoną liczbę sekund na wyjście. Jeśli ten czas upłynie, zostanie SIGKILL wysłany, a proces zostanie siłą zatrzymany (ponieważ procesy nie mogą zignorować SIGKILL).

W programie Supervisor limit czasu jest ustawiany przez opcjestopwaitsecs. Dokumenty Laravel mają to ustawione na jedną godzinę (w sekundach) w swoim przykładzie. Możesz to obniżyć, jeśli Twoje zadania nie działają długo i nigdy nie będą potrzebować godziny, aby je ukończyć.

Korzystanie z sygnałów w naszym kodzie

Zobaczmy, jak sami zaimplementować sygnały!

Zadania są już w pełni zakończone, gdy przychodzi sygnał, aby je zatrzymać. Z punktu widzenia nas, programistów, jesteśmy bardziej skłonni do przechwytywania sygnałów z naszych rzemieślniczych poleceń.

Pierwszą rzeczą, którą zobaczymy, jest to, jak sygnał natychmiast

zatrzyma proces (uruchomione polecenie).Zauważ, że będę tutaj używał zamiennie "procesu" i "polecenia". Uruchomienie polecenia takiego jak php artisan whatever uruchamia proces PHP. Ten proces uruchamia artisan, który uruchamia framework, uruchamia nasze polecenie, yadda yadda yadda. Chodzi o to, że oba słowa tu działają!

Stworzyłem polecenie LongCommand i uśpiłem je przez 10 sekund (tak jak 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'));
}

Jeśli uruchomię to przez artisan longtime i użyjęctrl+c, zatrzyma się natychmiast:

starting long command 14:27:59
^C%   

Nie było danych wyjściowych pokazujących, że kończy polecenie!

Pułapka

Możemy "uwięzić" - nasłuchiwać - sygnału. Pozwala nam to uruchomić kod przed zakończeniem polecenia.

Pułapka lets you capture a signal, and do something in response, but it will then exit immediately.

To może być błąd, nie jestem całkowicie pewien!

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'));
}

We trap SIGINT: and SIGTERM and just echo out some information. This gives us a hook to run some cleanup code before exiting!

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

The signal SIGINT: was "trapped" but it still stopped the process! We get similar behavior for SIGTERM:

starting long command: 14:33:39
sigint received 

Możemy więc reagować na sygnał, ale nie możemy faktycznie zatrzymać procesu przed wyjściem po uruchomieniu wywołania zwrotnego.

Byłoby przydatne, gdybyśmy mogli zignorować sygnał, dopóki nie będziemy gotowi do wyjścia! Na szczęście możemy.

❤️ Fly.io Laravel

Umieść swoje serwery blisko użytkowników i podziwiaj szybkość bliskości. Wdrażaj globalnie w Fly w kilka minut!

Wdróż swoją aplikację Laravel!  →

Implementacja SignalableCommandInterface

Jeśli nasze polecenie implementuje Symfony's SignalableCommandInterface, możemy uzyskać polecenie do zakończenia działania przed jego wyjściem.

(Możesz powiedzieć, że to rzecz Symfony, ponieważ nie nazywa się to czymś przyjemnym, jak, powiedzmy, Signalable).

Na pierwszy rzut oka wygląda na to, że działa to tak samo jak $this->trap() metoda. Jeśli jednak mamy return false w naszym przewodniku, polecenie jest w stanie dokończyć swoją pracę.

Oto jak to wygląda:

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

Ponieważ procedura obsługi sygnału powraca false, nasze polecenie jest w stanie zakończyć. Jak to działa, jest tylko szczegółem implementacji obsługi SignalableCommandInterface Symfony - mówi kodowi, aby nie działał exit($statusCode);.

Po wdrożeniu tego możemy zobaczyć nasze "gotowe..." Linia jest uruchomiona:

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

Możesz zauważyć, że tak naprawdę nie spaliśmy przez 30 sekund! Jest to specyficzne dla użycia sleep() do testowania. Używanie sygnałów faktycznie skraca aktualnie uruchomione sleep() połączenia, więc jeśli Twój kod opiera się na tym, może to być problem!

Po co to robić?

Przydatnym wzorcem do tego jest wykonanie pewnych prac porządkowych przed wyjściem:

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

Możesz teraz uruchomić kod czyszczenia w metodzie handleSignal() lub po pętli while() . Jest to świetne rozwiązanie do usuwania plików tymczasowych, zapewniania integralności danych, gdy polecenie jest wyłączone, zamykania połączeń sieciowych i wielu innych rzeczy!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297