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ą metody start_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 GenServerPingPong.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ć odpalone
  • worker – to metoda, tworząca proces jako proces pracujący. To znaczy taki wykonujący pracę. Możemy zamiast worker/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ący PingPong.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 (jako atom), która zostanie z danego modułu wywołana na starcie. Domyślna wartość :start_link. Jakbyśmy na przykład w PingPong.Server pozostali przy metodzie start 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 to 5000.
    • modules – lista jednoelementowa, zawiera moduł który powinien być wywołany, ale tylko i wyłącznie wtedy kiedy nasz proces jest Supervisor albo GenServer. Domyślnie jest to nazwa modułu z pierwszego parametru worker/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ślnie 3 restarty.
    • :max_seconds – przedział czasu w którym może nastąpić :max_restarts. Domyślnie 5 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.

1 KOMENTARZ

Comments are closed.