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 zostanie GenServer, zmienna __MODULE__ odwołuje się do aktualnego/bieżącego modułu.
  • 0 – By uprościć to jest to argument przekazany do init, a domyślnie jest on traktowany jako stan. W naszym starym PingPongServer to była liczba otwyrzmanych pingów.
  • name: pingpong – nadaje nazwę serwerowi, dzięki czemu możemy słać wiadomości przez send :pingpong a nie send 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ń :)

1 KOMENTARZ

Comments are closed.