Miało nie być trzech postów o supervisor, ale idealnie mi się wszystko składa w kupę i ten post był wymagany do tego by za tydzień nie rozbijać tematu na dwa posty. Przez ostatnie dwa tygodnie nauczyliśmy się tworzyć nadzorców z poziomu modułu jak i z iex. Dziś zaczynamy drogę do końca nauki podstaw elixira. Od dzisiaj będziemy pracować z narzędziem mix o którym wspomniałem już na samym początku. Naszym celem będzie zaprzyjaźnienie się z tym narzędziem, zobaczenie co ono oferuje, i powoli wejście w tworzenie aplikacji.

W 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 do artykułu w przyszłym tygodniu. Dodatkowo będziemy musieli zmodyfikować nasz PingPong.Server by działał on trochę bardziej dynamiczniej – tak byśmy mogli mieć jego dwie instancje działające na raz. Aktualnie to nie jest możliwe ze względu na przypisanie GenSever to atom :pingpong.

Modyfikacja PingPong.Server

Naszym pierwszym krokiem będzie tak stworzenie PingPong.Server byśmy mogli posiadać dwie instancje tego serwera, które nie będą się gryzły między sobą.

W tym celu zróbmy najprostszą z możliwych rzeczy. Zaś inne opcje jakie możemy zrobić opiszę kiedy indziej. Musimy zmodyfikować dwie metody, start_link/0 oraz ping/0 tak by przyjmowały jeden dodatkowy parametr – name:

defmodule PingPong.Server do
    use GenServer

    def start_link(name) do
        GenServer.start_link(__MODULE__, 0, name: name)
    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(name) do
        GenServer.call(name, {:ping, "ping"}, 5000)
    end
end

Nowy projekt, nowa aplikacja

Mając gotowy PingPong.Server, możemy przystąpić do działania. Stwórzmy sobie aplikację z wykorzystaniem mix. Nazwijmy ją sup_app. Normalnie komendą jaką byśmy wykorzystali do jej stworznia byłaby komenda:

mix new sup_app

Jednak, my wiemy co chcemy. Chcemy stworzyć drzewko nadzorcy. Jak się okazuje, mix nam to trochę ułatwia i jak przekażemy parametr --sup to stworzy on nam odpowiednią klasę i wstępną implementację odpalania supervisor z wykorzystaniem kodu znanego nam z poprzedniego rozdziału.

mix new sup_app --sup

Te dwie komendy różnią zawartościcą pliku sup_app.ex w katalogu /lib. W pierwszej opcji, jest to pusty plik, w drugiej wygląda on tak:

defmodule SupApp do
  use Application

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Define workers and child supervisors to be supervised
    children = [
      # Starts a worker by calling: SupApp.Worker.start_link(arg1, arg2, arg3)
      # worker(SupApp.Worker, [arg1, arg2, arg3]),
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: SubTest2.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Kod chyba jest zrozumiały? Tutaj wykorzystujemy use Application. O tym jeszcze napiszę. Ale pewnie dopiero za dwa/trzy tygodnie. Na razie jedyne co musimy wiedzieć to, to, że Application jest odpowiedzialna za wystartowanie i zatrzymanie jakieś określonej funkcjonalności, która może być re-wykorzystywana w innych systemach. Blah blah blah… aplikacja to aplikacja (tak to specjalnie zostało napisane).

Niezależnie od Application, powinniście zauważyć kod odpowiedzialny za tworzenie nadzorcy, który wygląda prawie jak 1-1 z tym co żeśmy napisali w zeszłym tygodniu. To co my teraz zrobimy, to go lekko doszlifujemy.

Stworzenie pierwszego PingPong.Server

Mając gotową aplikację, stwórzmy nasz pierwszy PingPong.Server. A następnie upewnijmy się w iex, że to działa. W tym celu, do /lib dodajmy zmodyfikowany plik PingPongServer.ex. Następnie musimy dodać nasz serwer jako worker. Robimy to poprzez dodanie wpisy do children:

worker(PingPong.Server, [:fromSupApp])

To co tutaj się dzieje:

  1. Dodajemy nowego pracownika
  2. Przekazujemy parametry do naszego serwera. Każdy osoby element to, nowy parametr w naszym start_link. My mamy jeden name, więc tylko w liście przekazujemy jeden element
  3. Pomijamy resztę opcji jako, że defaultowo będą one dobre

Mając stworzonego worker, możemy przetestować nasze rozwiązanie:

iex -S mix

A następnie:

iex> Process.whereis :fromSupApp
#PID<0.120.0>

iex> PingPong.Server.ping :fromSupApp
#0 Received: ping from {#PID<0.121.0>, #Reference<0.0.4.369>}
{:pong, "...pong #0"}

Jak to nam działa, to możemy wykonać std kod już nasz do ubijana procesu i sprawdzania czy aby na pewno wstał:

Process.whereis(:fromSupApp)
Process.whereis(:fromSupApp) |> Process.exit(:kill)
Process.whereis(:fromSupApp)
Process.whereis(:fromSupApp) |> Process.exit(:kill)
Process.whereis(:fromSupApp)

Jak nam działa to super. Pozostaje nam teraz dodanie supervisor jako dziecka.

Dodanie supervisor

Weźmy nasz moduł nadzorcy z pierwszego posta, dodajmy go do /lib i lekko go zmodyfikujmy pod nowe parametry PingPong.Server. Czyli z:

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

Stwórzmy to:

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, [:fromPingPongSup])
    ]

    supervise(children, strategy: :one_for_one)
  end
end

Jedyną różnicą jest definicja worker – podajemy nazwę i to nazwę różniącą się od :fromSupApp.

Dlaczego dodaliśmy ten moduł i go modyfikowaliśmy? Po to by teraz dodać nadzorcę jako dziecko naszej aplikacji! :) how cool is that? Nadzorca dla nadzorcy! Mając odpowiednio zmodyfikowany plik, możemy przystąpić do zabawy. Nasz kod do rejestrowania dzieci powinien teraz wyglądać następująco:

children = [
  worker(PingPong.Server, [:fromSupApp]),
  supervisor(PingPong.Supervisor, [])
]

Tym razem wykorzystaliśmy supervisor/3 – aż chce się sprawdzić jak to się zachowa i czy to zadziała. Nie ma co czekać :)

iex -S mix

I zróbmy dwa proste testy:

iex> PingPong.Server.ping :fromPingPongSup
#0 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.745>}
{:pong, "...pong #0"}

iex> PingPong.Server.ping :fromSupApp
#0 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.759>}
{:pong, "...pong #0"}

iex> PingPong.Server.ping :fromSupApp
#1 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.764>}
{:pong, "...pong #1"}

iex> PingPong.Server.ping :fromPingPongSup
#1 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.769>}
{:pong, "...pong #1"}

Zwróćmy uwagę na dwie rzeczy:

  1. Że wykonujemy ten kod z tego samego proces #PID<0.123.0>
  2. Że wysyłamy go do różnych serwerów i każdy zwraca swój własny count

To co teraz możemy zrobić to ubić jeden z procesów i zobaczyć co się stanie:

Process.whereis(:fromPingPongSup) |> Process.exit(:kill)
Process.whereis(:fromPingPongSup)

Jeżeli wykonamy kolejny ping to zobaczymy, że count został zresetowany:

iex> PingPong.Server.ping :fromPingPongSup
#0 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.780>}
{:pong, "...pong #0"}

Ale nie dla :fromSupApp

iex> PingPong.Server.ping :fromSupApp
#2 Received: ping from {#PID<0.123.0>, #Reference<0.0.8.785>}
{:pong, "...pong #2"}

Co dalej

Teraz, można się pobawić, pododawać kolejne serwery PingPing, zobaczyć różne strategie, różne wartości restartowania. Dla przykładu, raz ubity proces dla serwera stworzonego przez PingPongSupervisor nigdy nie powinien się podnieść:

worker(PingPong.Server, [:fromPingPongSup], restart: :temporary)

I test:

iex> PingPong.Server.ping :fromPingPongSup
#0 Received: ping from {#PID<0.123.0>, #Reference<0.0.6.906>}
{:pong, "...pong #0"}

iex> Process.whereis(:fromPingPongSup) |> Process.exit(:kill)
true

iex> PingPong.Server.ping :fromPingPongSup
** (exit) exited in: GenServer.call(:fromPingPongSup, {:ping, "ping"}, 5000)
    ** (EXIT) no process
    (elixir) lib/gen_server.ex:596: GenServer.call/3

Podsumowanie

Nie było tutaj nic trudnego ani nic nowego. Wszystko co dzisiaj przeczytaliście powinniście już wiedzieć z poprzednich postów. To co jednak dowiedzieliśmy się, to, to, że możemy stworzyć aplikację za pomocą mix od razu ze wsparciem Supervisor. Zaś kod tego supervisor wygląda tak samo jak kod który wykorzystywaliśmy w poprzednim tygodniu przy pisaniu skryptu.

Dodatkowo mogliśmy się pobawić jak wpływają różne strategie i różne sposoby restartowania procesów na nasz kod. Także stworzyliśmy aplikację, która posłuży nam jako model do następnych dwóch jak nie więcej postów.

A teraz bądźcie ze mną szczerzy, czy GenServer i Supervisor jest przez was zrozumiały? Czy czegoś wam tutaj brakuje? Bo to są building blocksy elixir i erlanga. Bez zrozumienia tego będzie ciężko :( Dajcie znać w komentarzach, dzięki!

3 KOMENTARZE

  1. “Dla przykładu, raz upity proces dla serwera stworzonego przez PingPongSupervisor nigdy nie powinien się podnieść […]” – zależy co pił :D

Comments are closed.