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 starymPingPongServer
to była liczba otwyrzmanych pingów.name: pingpong
– nadaje nazwę serwerowi, dzięki czemu możemy słać wiadomości przezsend :pingpong
a 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.