A GenServer (általános szerver) egy viselkedés, amely az Elixir-folyamatok közötti gyakori kliens-szerver interakciókat absztrahálja.
Emlékszel a fogadó ciklusra, amikor a folyamatokról tanultunk? A GenServer viselkedés absztrakciókat kínál az ilyen ciklusok megvalósításához, valamint ahhoz, hogy üzeneteket váltsunk egy ilyen ciklust futtató folyamattal. Megkönnyíti az állapot megtartását és az aszinkron kód futtatását.
Vigyázat, a GenServer név több dolgot is jelöl. Ezzel illetik azt a modult is, amely használja a GenServer viselkedést, valamint azt a folyamatot is, amelyet egy ilyen modul indított el.
A GenServer viselkedés egy kötelező visszahívást definiál (init/1), valamint néhány érdekes opcionális visszahívást: handle_call/3, handle_cast/2 és handle_info/3. A GenServer-t használó kliensek nem hívhatják meg ezeket a visszahívásokat közvetlenül. Ehelyett a GenServer modul olyan függvényeket kínál, amelyekkel a kliensek kommunikálhatnak egy GenServer folyamattal.
Gyakran egyetlen modul definiálja egyszerre a kliens API-t, azaz a függvények halmazát, amelyeket az Elixir-alkalmazásod más részei hívhatnak meg, hogy kommunikáljanak ezzel a GenServer folyamattal, és a szerveroldali visszahívások implementációit, amelyek a GenServer logikáját tartalmazzák.
Nézzünk először egy egyszerű példát egy GenServer-re, és utána nézzük meg, mit jelent az egyes visszahívások.
Ez egy példaszerver, amely egy hosszú autós utazás során elviseli az idegesítő utasok ismétlődő kérdezősködését, pontosabban a „Megérkeztünk már?” kérdést. Nyilvántartja, hányszor tették fel ezt a kérdést, és egyre idegesebb válaszokat ad rá.
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1Egy szervert a GenServer.start/3 vagy a GenServer.start_link/3 hívásával indíthatsz el. A két függvény közti különbségről a linkek fogalmánál tanultunk.
Ez a két függvény:
GenServer viselkedést.init_arg néven. Ahogy a neve is sugallja, ez az argumentum az init/1 visszahíváshoz kerül átadásra.Ha egy szervert a GenServer.start/3 vagy a GenServer.start_link/3 meghívásával indítasz el, az blokkoló módon hívja meg az init/1 visszahívást. Az init/1 visszatérési értéke dönti el, hogy a szerver sikeresen elindulhat-e.
Az init/1 visszahívás rendszerint az alábbi értékek egyikét adja vissza:
{:ok, state}. A szerver elindítja a fogadó ciklusát, és a state lesz a kezdő állapota. A state bármilyen típusú lehet.{:stop, reason}. A reason bármilyen típusú lehet. A szerver nem indítja el a fogadó ciklusát. A folyamat a megadott okkal lép ki.Vannak ennél haladóbb lehetőségek is, amelyeket most nem tárgyalunk.
Ha a szerver fogadó ciklusa elindul, a GenServer.start/3 és a GenServer.start_link/3 függvény egy {:ok, pid} tuple-t ad vissza. Ellenkező esetben {:error, reason} a visszatérési értékük
handle_call/3Egy választ igénylő üzenetet a GenServer.call/2 segítségével küldhetsz el egy szerverfolyamatnak. Ez a függvény első argumentumként egy futó szerverfolyamat pid-jét várja, másodikként pedig az üzenetet. Az üzenet bármilyen típusú lehet.
A handle_call/3 visszahívás felelős a szinkron üzenetek kezeléséért és megválaszolásáért. Három argumentumot kap:
message - a GenServer.call/2 második argumentumaként átadott érték.from - a GenServer.call/2 függvényt hívó folyamat pid-je. Legtöbbször figyelmen kívül hagyható.state - a szerver aktuális állapota. Ne feledd, hogy a kezdőértékét az init/1 visszahívásban állítottad be.A handle_call/3 visszahívás rendszerint egy háromelemű tuple-t ad vissza: {:reply, reply, state}. Ez azt jelenti, hogy a tuple második elemét, a bármilyen típusú reply-t visszaküldi a hívónak. A tuple harmadik eleme, a state, a szerver új állapota az üzenet kezelése után.
Vannak ennél haladóbb lehetőségek is, amelyeket most nem tárgyalunk.
Hogy a neve alapján megjegyezd, mit csinál ez a visszahívás, gondolj arra, mintha felhívnál valakit telefonon.
Ha az illető elérhető, azonnal (szinkron módon) választ kapsz.
handle_cast/2Egy választ nem igénylő üzenetet a GenServer.cast/2 segítségével küldhetsz el egy szerverfolyamatnak. Az argumentumai megegyeznek a GenServer.call/2 argumentumaival.
A handle_cast/2 visszahívás felelős ezeknek az üzeneteknek a kezeléséért. Két argumentumot kap, a message-t és a state-et, amelyek ugyanazok, mint a handle_call/3 visszahívásban (a from kivételével).
A handle_cast/2 visszahívás rendszerint egy kételemű tuple-t ad vissza: {:noreply, state}.
Vannak ennél haladóbb lehetőségek is, amelyeket most nem tárgyalunk.
Hogy a neve alapján megjegyezd, mit csinál ez a visszahívás, jusson eszedbe, hogy a „to cast” angol kifejezés azt is jelenti, hogy „dobni”.
Ha egy üzenetet palackba zárva a tengerbe dobsz, nem számítasz rá, hogy azonnal választ kapsz, vagy hogy egyáltalán valaha kapsz.
call-t vagy a cast-ot?Szinte mindig a call-t használd, még akkor is, ha a kliensoldali kódodnak nincs szüksége a szerver válaszára.
A call használata megvárja a választ, ami visszanyomási mechanizmusként szolgál (hogy megakadályozza, hogy a kliensek egyszerre túl sok üzenetet küldjenek). A szervertől kapott válasz az egyetlen módja annak is, hogy biztos lehess benne, hogy a szerver megkapta és kezelte a kliens üzenetét.
handle_info/2Az üzenetek más módon is a szerver postaládájába kerülhetnek, nem csak a GenServer.call/2 vagy a GenServer.cast/2 meghívásával, például a sima send/2 függvény hívásával.
Az ilyen üzenetek kezelésére a handle_info/2 visszahívást használd. Ez a visszahívás pontosan ugyanúgy működik, mint a handle_cast/2.
A GenServer viselkedés biztosít egy mindent elkapó alapértelmezett implementációt a handle_info/2-höz, amely naplózza a nem várt üzenetekkel kapcsolatos hibákat. Ha felülírod ezt az alapértelmezett implementációt, mindig gondoskodj róla, hogy a saját mindent elkapó implementációd is meglegyen. Ha elfelejted, a szerver összeomlik, amikor nem várt üzenetet kap.
A fent leírt négy visszahívás visszatérési értéke kiegészíthető még egy tuple-elemmel, egy időtúllépéssel. Például az init/1-ből {:ok, state} helyett adj vissza {:ok, state, timeout} értéket.
Az időtúllépéssel azt lehet érzékelni, hogy egy adott ideig nem érkezett üzenet a postaládába. Ha a szerver valamelyik visszahívásából időtúllépést ad vissza, és a megadott számú ezredmásodperc eltelik úgy, hogy nem érkezik üzenet, akkor a handle_info/2 hívódik meg, első argumentumként :timeout-tal.