W zeszłym tygodniu w Elixir zrobiliśmy sobie prosty PingPongServer działający synchronicznie. To znaczy, że kiedy wysłaliśmy wiadomości ping czekaliśmy na odpowiedź pong. Czekaliśmy w nieskończoność. Przy czym to, my byliśmy odpowiedzialni za uruchomienie serwera jak i podtrzymywanie go w pętli. Wysyłanie było dość proste, wystarczyło znać odpowiedni PID i tyle. Ale by do tego dotrzeć naprawdę trzeba było zaprogramować mały wzorzec. W zależności od złożoności problemu albo mało roboty albo naprawdę dużo.
Dlatego też elixir przychodzi z modułem zachowań – behaviours (w końcu poprawna brytyjska pisownia a nie jakieś behaviors ;)) – GenServer. Zanim jednak przejdziemy do implementacji GenServer, powiedzmy sobie co to są zachowania.
Behaviours
Jest to tak dziwne dla ludzi z poza światka Elixir, że jak eldhash mi ostatnio o tym wspomniał to aż się go zapytałem co to ;) Ogólnie, mam wrażenie, że niektóre rzeczy z elixirem to jak z crossfitem – własne nazewnictwo by było ql i hipstersko :)
Behaviours to mniej więcej interfejs w rozumieniu C#. Czyli metody które muszą być zaimplementowane. W przypadku GenServer jest ich aż 6 :) Na szczęście nie musimy ich implementować ;) ktoś zrobił to za nas. Ogólnie o behaviours będę jeszcze pisał – gdyż ich deklaracja i sposób tworzenia wybiega poza ten post – zresztą, sami zobaczycie, implementacja behaviours rozpoczyna się od @behaviour Nazwa, a my będziemy wykorzystywać use.
GenServer
To moduł, który deklaruje i implementuje behaviours – zakręcone co? No ale dzięki temu my nie musimy pisać za każdym razem 6 różnych metod. Dzięki czemu będziemy musieli jedynie przeciążyć te metody które nas interesują. Ba, nic nie musimy, możemy nawet nic nie implementować ;)
W celu wykorzystania GenServer, wystarczy, że pod deklaracją naszego modułu dodamy:
use GenServer
I to wystarczy. Tam pod spodem więcej magii się dzieje, ale o tym co się dzieje przy use innym razem. Na teraz ważne jest to, że use powoduje, iż wykorzystujemy moduł, który implementuje nam behaviours tego samego modułu. Incepcja ;)
Eksplorację GenServer pozostawię wam – każda z metod może przyjmować jak i zwracać różne wartości. Nie byłbym w stanie tego tutaj wam wszystkiego opisać. Zamist tego, skoncetrujemy się na prostym przykładzie przepisania PingPongServer na GenServer.
W tym celu stwórzmy plik ping_ping_server_gen.exs i nadajmy mu prostą strukturę:
defmodule PingPongServer do use GenServer end
Możemy to odpalić w iex i zobaczyć, że śmiga bez błędów :)
Teraz możemy zająć się implementacją naszego serwera. Podstawową metodą jest metoda startująca serwer start_link – to ona odpala naszego loopa z poprzedniego tygodnia. My u siebie możemy dodać metodę start która wywoła start_link:
def start() do GenServer.start_link(__MODULE__, 0, name: :pingpong) end
Ogólnie nie musimy tego robić, ale metodę i tak trzeba wywołać to łatwiej to zrobić za pomocą start niż pamiętać te wszystkie parametry:
__MODULE__– Moduł pod który podpięty zostanieGenServer, zmienna__MODULE__odwołuje się do aktualnego/bieżącego modułu.0– By uprościć to jest to argument przekazany doinit, a domyślnie jest on traktowany jako stan. W naszym starymPingPongServerto była liczba otwyrzmanych pingów.name: pingpong– nadaje nazwę serwerowi, dzięki czemu możemy słać wiadomości przezsend :pingponga niesend PID. W szczególności przydatne kiedy mamy Supervisora – coś co monitoruje nasz proces i wykonuje operacje jak on siądzie – restart procesu == nowy PID
To teraz pora wysłać pinga do naszego serwera. Ponownie robimy skrót by nie musieć pisać wszystkiego jak przy start_link. Teraz robimy to dla call:
def ping() do
GenServer.call(:pingpong, {:ping, "ping"}, 5000)
end
Czyli wysyłamy wiadomość ping do serwera :pingpong i czekamy na odpowiedź do 5 sekund.
Pozostaje nam tylko odebrać tą wiadomość i zwrócić odpowiedź. I tutaj jest pierwszy behaviour który my zaimplementujemy – handle_call, implementacja tego jest tak samo prosta jak deklaracja metody:
def handle_call({:ping, msg}, from, state) do
:timer.sleep(1000)
IO.puts "##{state} Received: #{msg} from #{inspect(from)}"
{:reply, {:pong, "...pong ##{state}"}, state + 1}
end
Mówimy na co czekamy a dwa pozostałe parametry to od kogo dostaliśmy i jaki jest aktualny stan (to co przy start_link na 0 ustawialiśmy). Następnie wyświetlamy info o otrzymanej wiadomości. Kończymy zaś zwróceniem wiadomości – jednej z możliwości jaką nam daje i na jaką nam zezwala handle_call. W tym wypadku, :reply znaczy, że wiadomość zostanie zwrócona, następnie treść wiadomości {:pong, "...pong ##{state}"} i na końcu nowy stan.
Całość wygląda tak:
defmodule PingPongServer do
use GenServer
def start() do
GenServer.start_link(__MODULE__, 0, name: :pingpong)
end
def handle_call({:ping, msg}, from, state) do
:timer.sleep(1000)
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
Wszystko zamknięte w 18 linijkach. I tak naprawdę zero magii. Dokładnie wiemy co robią poszczególne metody, dzięki temu, że tydzień temu napisaliśmy sami dosłownie taki sam serwer. Jest on oczywiście bardziej rozbudowany i mamy dużo więcej możliwości out of the box. Jednak zasada działania jest taka sama.
Podsumowanie
GenServer to jeden z podstawowych modułów, bez którego raczej się aplikacja nie obejdzie. Jeżeli nie my to jakaś biblioteka będzie z niego korzystała. Daje on przyjemną i prostą w wykorzystaniu abstrakcję nad komunikacją asynchroniczną i synchroniczną pomiędzy procesami. Do tego idealnie współpracuje z Supervisors.
Inaczej, po co mamy cały czas pisać to samo? Skoro mamy przetestowany w boju na serwerach produkcyjnych kod GenServer, który mamy gwarancję, że działa. Wystarczy zaimplementować tylko kilka metod.
Kolejny post o elixir i kolejny który naprawdę przyniósł mi radość :) gdyby nie to, że jednak chcę spędzić czas z rodziną to bym pewnie jeszcze dopisywał dodatkowe paragrafy do niego ;) a więc do przeczytania o elixr za tydzień :)















[…] Elixir #14: GenServer […]
Comments are closed.