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ł (SIGQUIT
SIGTERM
róż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.
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!