Poznawszy już GenServer, jak i tematy powiązane (use
, __using__/1
, require
). Pora spojrzeć na Supervisor. Jak pamiętacie, elixir działa w oparciu o maszynę wirtualną erlanga – BEAM. Nasz kod zaś działa w procesie. Taki nasz proces, może odgrywać rolę supervisor – zarządczy/nadzorcy, który odpowiedzialny jest za uruchamianie, zatrzymywanie i kontrolowanie procesów (zwanych dziećmi) – co i jak, zależne jest od strategii jaką wybierzmy dla Supervisora.
Z Supervisor możemy zarówno skorzystać w skrypcie jak i w aplikacji (za pomocą mix i osobnego modułu). Dziś przedstawię sposób tworzenia za pomocą modułu Supervisora, który będzie nadzorował działanie wcześniej przez nas napisanego Ping Pong Server. W tym celu musimy wprowadzić kilka poprawek do naszego serwera. Po pierwsze, metoda start
musi zostać zamieniona na start_link
– jest to konwencja nazewnictwa (można to nadpisać w ustawieniach), dzięki temu supervisior będzie wiedział co ma odpalić. Do tego wywalmy :timer.sleep
i zmieńmy nazwę z PongPongServer
na PingPong.Server
:
defmodule PingPong.Server do use GenServer def start_link() do GenServer.start_link(__MODULE__, 0, name: :pingpong) end def handle_call({:ping, msg}, from, state) do IO.puts "##{state} Received: #{msg} from #{inspect(from)}" {:reply, {:pong, "...pong ##{state}"}, state + 1} end def ping() do GenServer.call(:pingpong, {:ping, "ping"}, 5000) end end
Mając kod gotowy, proponuje go zapisać jako PingPongServer.exs
, przyda się później – głównie do copy-paste. Możemy zatem przystąpić do zabawy z supervisorem.
Co To jest Supervisor?
Supervisor, to proces nadzorujący inne procesy. Te procesy, są to procesy dzieci. Można sobie to wyobrazić jako drzewo. Supervisor jest korzeniem, a wszystko co potem zostanie zbudowane jest jego dzieckiem. Dziecko zaś może być pracownikiem lub nadzorcą. To drzewo ułatwia nam znacznie tworzenie systemów odpornych na awarie. Głównie przez to, że jesteśmy wstanie reagować na to co się może wydarzyć odpowiednią strategią.
W Supervisor mamy dostępne kilka podstawowych strategii:
:one_for_one
– jeżeli padnie proces dziecka, tylko ten proces pada i tylko ten proces jest wystartowany ponownie.:one_for_all
– jeżeli padnie proces dziecka, to wszystkie pozostałe procesy są terminowane, i następnie wszystkie ponownie ładowane.:rest_for_one
– jeżeli padnie proces dziecka, to ten proces jest restartowany oraz każdy inny który został wystartowany po procesie który padł.:simple_one_for_one
– tak jak:one_for_one
, ale dla dynamicznie podpinanych dzieci (za pomocą metodystart_child/2
) – z tego korzystać na razie nie będziemy.
Do tego tego dochodzi jeszcze sposób restartowania procesów (ale to już jest per definicja dziecka a nie per Supervisor):
:permanent
– proces dziecka jest zawsze restartowany:temporary
– proces dziecka nigdy nie jest restartowany:transient
– proces jest restartowany, wtedy i tylko wtedy kiedy jest on zakończony nienormalnie (wyjątek itp.)
Parametry strategii jest wymagany. Może jednak być napisany przez strategię restartowania poszczególnego dziecka. Domyślna wartość restartowania ustawiona jest na :permanent
.
Supervisor jako moduł
NA początku stwórzmy osoby moduł, który będzie nam zarządzał naszym procesem GenServer
– PingPong.Server
. W tym celu wykorzystamy znane nam już makro use oraz jeszcze nie znany nam moduł Supervisor
. Na początku stwórzmy prosty moduł z odpowiednim use:
defmodule PingPong.Supervisor do use Supervisor end
Zapiszmy nasz nowy skrypt jako sup.exs
(skracam nazwy by potem było łatwiej) i odpalmy go za pomocą iex sup.exs
. Powinno to spowodować błąd:
warning: undefined behaviour function init/1 (for behaviour Supervisor)
Supervisor
różni się tym od GenServer
, że on nie implementuje zachować. On je przekazuje dalej. Zaś to co robi, to jedynie importuje nam moduł Supervisor.Spec
, który jest wymagany jeżeli chcemy tworzyć Supervisor
– umożliwia on stworzenie definicji/schematu Supervisor
.
Wiemy już, że nasz Supervisor
będzie potrzebował przynajmniej jednej metody – init/1
. Metoda ta, odpowiedzialna jest za zwrócenie informacji na temat Supervisor
– jaka ma być strategia restartowania, w jaki sposób ma następować restart, oraz jakie dzieci muszą być stworzone. Dokumentacja do metody dostępna jest w dokumentacji erlanga, na elixir nie jest ona dokończona. Zaś co powinna zwrócić metoda init/1 (schemat Supervisor
) można znaleźć w opisie supervise/2
.
Z dokumentacji, dowiadujemy się, że metoda ta, jest wołana przez start_link
– czyli samo jej wywołanie, nic nam nie da. Zwróci nam konfigurację ale nic się nie stanie. Dopiero implementacja start_link
wie co ma z tym zrobić i jak. I to nie implementacja Elixira ale Erlanga – znów, dokumentacja Erlanga trochę więcej światło na to rzuci. Dzięki temu jesteśmy wstanie wywnioskować, że nasz Supevisor
potrzebuje przynajmniej minimum dwóch metod: init/1
i start_link/2,3
.
Na szczęście, nasze makro use
, trochę nam ułatwia i nie musimy implementować start_link/2,3
– zaś musimy te metody wywołać. Podobnie jak w PingPong.Server
wystarczy, że stworzymy prostą metodę która będzie wykonywała nam start_link
. Czyli nasz moduł wygląda teraz tak:
defmodule PingPong.Supervisor do use Supervisor # http://erlang.org/doc/man/supervisor.html#start_link-2 def start_link do Supervisor.start_link(__MODULE__, []) end # http://erlang.org/doc/man/supervisor.html#Module:init-1 def init(args) do end end
To co robi start_link
to wywołuje start_link/2
podając aktualny moduł __MODULE__
i pustą listę argumentów. Ta lista zostanie przekazana do naszego init/1
.
Pozostaje kwestia zaimplementowania init/1
tak by zwracał nam to co ma zwracać – opcje, lista dzieci, strategie, ogólnie schemat Supervisor
. Nasz kod będzie wyglądał następująco:
def init(args) do children = [ worker(PingPong.Server, []) ] supervise(children, strategy: :one_for_one) end
Gdzie
children
– to lista procesów które mają być odpaloneworker
– to metoda, tworząca proces jako proces pracujący. To znaczy taki wykonujący pracę. Możemy zamiastworker/3
tutaj użyćsupervisor/3
– stworzy to nam proces, który będzie dzieckiem nadzorcy i zarazem nadzorcom dla kolejnych procesów utworzony przez siebie. W tym wypadku mówimy, że naszym dzieckiem jest proces pracującyPingPong.Server
i nie przyjmuje on żadnych parametrów wejściowych. Dodatkowo pominęliśmy opcjonalny parametr, opcji w którym możemy ustalić:id
– dla nas, byśmy mogli lepiej identyfikować pracownika (dziecko pracownik… dziwnie to brzmi). Domyślna wartość to nazwa naszego modułu.function
– metoda (jakoatom
), która zostanie z danego modułu wywołana na starcie. Domyślna wartość:start_link
. Jakbyśmy na przykład wPingPong.Server
pozostali przy metodziestart
to tutaj musielibyśmy to określić.shutdown
– określa jak nasz proces ma być ubity. Są trzy opcje:liczba
– próbujemy zamknąć przez okres liczba, jak się nie udaje to ubijamy;:infinity
– czekamy w nieskończoność, bardziej dla nadzorców niż pracowników;:brutal_kill
– z miejsca jest ubijany. Domyślna wartość to parametru to5000
.modules
– lista jednoelementowa, zawiera moduł który powinien być wywołany, ale tylko i wyłącznie wtedy kiedy nasz proces jestSupervisor
alboGenServer
. Domyślnie jest to nazwa modułu z pierwszego parametruworker/3
.
supervise
– metoda, która bierze listę dzieci (zarówno pracowników jak i nadzorców) i ustala dla nich strategię radzenia sobie w razie awarii. Zwraca specyfikację nadzorcy. Oprócz wymienionych parametrów, możemy przekazać jeszcze dwa dodatkowe::max_restarts
– maksymalna liczba restartów procesu w danym przedziale czasowym (:max_seconds
). Domyślnie3
restarty.:max_seconds
– przedział czasu w którym może nastąpić:max_restarts
. Domyślnie5
sekund.
Cały nasz kod wygląda więc tak:
defmodule PingPong.Supervisor do use Supervisor # http://erlang.org/doc/man/supervisor.html#start_link-2 def start_link do Supervisor.start_link(__MODULE__, []) end # http://erlang.org/doc/man/supervisor.html#Module:init-1 def init(args) do children = [ worker(PingPong.Server, []) ] supervise(children, strategy: :one_for_one) end end
Sprawdzenie działania
Jak mamy nasz PingPong.Server
i teraz jeszcze PingPong.Supervisor
to dla naszej wygody, dodajmy te dwa moduły do jednego pliku sol.exs
i załadujmy plik w iex:
iex sol.exs
Zróbmy kilka testów – byśmy wiedzieli z czym mamy doczynienia:
iex> PingPong.Supervisor.init([]) {:ok, {{:one_for_one, 3, 5}, [{PingPong.Server, {PingPong.Server, :start_link, []}, :permanent, 5000, :worker, [PingPong.Server]}]}}
Czyli dostajemy tuples, gdzie mamy wszystko to o czym pisaliśmy wcześniej. Dla przykładu {:one_for_one, 3, 5}
to strategia w trakcie awarii, liczba prób i liczba sekund. Spróbujmy wywołać ping/0
:
iex> PingPong.Server.ping ** (exit) exited in: GenServer.call(:pingpong, {:ping, "ping"}, 5000) ** (EXIT) no process (elixir) lib/gen_server.ex:596: GenServer.call/3
Próba ping/0
nie udała się bo nie mamy wystartowanego serwera, możemy to jeszcze zweryfikować:
iex> Process.whereis :pingpong nil
Jak chcemy to (opcjonalnie) też możemy sprawdzić czy PoingPong.Server
będzie nam działał za pomocą:
iex>PingPong.Server.start_link iex>PingPong.Server.ping
Jak to zrobicie to ubijcie proces servera:
iex>Process.whereis(:pingpong) |> Process.exit(:kill)
Teraz PingPong.Server.ping nie powinien działać.
Zweryfikowaliśmy, że PingPong.Supervisor
zwraca to co trzeba oraz opcjonalnie, że PingPong.Server
działa. Odpalmy teraz proces supervisora:
iex> PingPong.Supervisor.start_link
Powinniśmy móc wysłać teraz ping/0
:
iex> PingPong.Server.ping #0 Received: ping from {#PID<0.87.0>, #Reference<0.0.8.452>} {:pong, "...pong #0"}
Sprawdźmy jaki jest PID
naszego servera :pingpong
:
iex> Process.whereis(:pingpong)
Teraz, by sprawdzić jak działa PingPong.Supervisor
, zróbmy serię poleceń:
Process.whereis(:pingpong) Process.whereis(:pingpong) |> Process.exit(:kill) Process.whereis(:pingpong) Process.whereis(:pingpong) |> Process.exit(:kill) Process.whereis(:pingpong)
Za każdy razem po ubiciu procesu, Process.whereis(:pingpong)
zwraca nam nowy PID
. A to znaczy, że supervisor działa jak należy. Za każdym razem jak tylko padnie nam proces PingPong.Server
, PingPong.Supervisor
podnosi go za nas.
Podsumowanie
Miał być krótki opis co i jak. Wyszedł długi ale za to dokładny i prawie wyczerpujący temat post. Pozostał mi do opisania jeszcze sposób wykorzystania Supervisor
bez modułu. To co się udało pokazać to, że możemy stworzyć proces nadzorcy, który w zależności od wybranej strategii będzie zarządzał pracownikami, którzy są procesami dziećmi.
Za każdym razem kiedy ubijaliśmy proces, on automatycznie stawał i był gotowy do pracy. W ten sposób osiągnęliśmy aplikację która jest odporna na awarię i nie musieliśmy się przy tym napracować. Całość dało się zamknąć w 12 linijkach kodu. Co moim zdaniem jest całkiem niezłym osiągnięciem.
Za tydzień, poruszę temat jak wykorzystać Supervisor bez jego implementacji.
[…] tym tygodniu wykorzystamy naszą wiedzę o supervisor i stworzymy aplikację wykorzystującą dwa sposoby tworzenia nadzorców. Ogólnie, celem jest stworzenie drzewka, które będzie nam potrzebne […]
Comments are closed.