A GenServer (általános szerver) egy viselkedés, amely elvonatkoztatja az Elixir-folyamatok közötti gyakori kliens-szerver interakciókat.
Emlékszel a receive-ciklusra, amikor a folyamatokról tanultunk? A GenServer viselkedés elvonatkoztatásokat nyújt az ilyen ciklusok megvalósításához, és ahhoz, hogy üzeneteket váltsunk egy ilyen ciklust futtató folyamattal. Megkönnyíti az állapot megőrzését és az aszinkron kód futtatását.
Vigyázat, a GenServer név túlterhelt. Arra a modulra is használják, amely használja a GenServer viselkedést, valamint arra a folyamatra, amelyet egy olyan modul indított el, amely használja a GenServer viselkedést.
A GenServer viselkedés egy kötelező visszahívást definiál, az init/1-et, 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 hivatottak közvetlenül meghívni ezeket a visszahívásokat. Ehelyett a GenServer modul olyan függvényeket biztosít, amelyekkel a kliensek kommunikálhatnak egy GenServer-folyamattal.
Gyakran egyetlen modul definiálja egyszerre a kliens API-t, azaz a függvények egy halmazát, amelyeket az Elixir-alkalmazásod más részei meghívhatnak, hogy kommunikáljanak ezzel a GenServer-folyamattal, és a szerver-visszahívások implementációit, amelyek ennek a GenServer-nek a logikáját tartalmazzák.
Nézzünk meg először egy egyszerű példát a GenServer-re, aztán megtanuljuk, hogy mit jelentenek az egyes visszahívások.
Ez egy példaszerver, amely válaszolni tud egy bosszantó utas ismétlődő faggatásaira egy hosszú autós út során, pontosabban a kérdésre: „megérkeztünk már?”. Nyilvántartja, hányszor tették fel ezt a kérdést, és egyre ingerültebb válaszokat ad.
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 meghívásával lehet elindítani. Ezen függvények közötti különbségről a linkek fogalomban tanultunk.
Ez a két függvény:
GenServer viselkedést.init_arg-nak nevezünk. Ahogy a neve is sugallja, ezt az argumentumot kapja meg az init/1 visszahívás.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 általában a következő értékek egyikét adja vissza:
{:ok, state}. A szerver elindítja a receive-ciklusát, és a state-et használja kezdő állapotként. A state bármilyen típusú lehet.{:stop, reason}. A reason bármilyen típusú lehet. A szerver nem indítja el a receive-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 receive-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}-t adnak vissza.
handle_call/3Egy választ igénylő üzenetet a GenServer.call/2-vel küldhetsz 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 - annak a folyamatnak a pid-je, amely a GenServer.call/2-t hívja. Legtöbbször figyelmen kívül hagyható ez az argumentum.state - a szerver aktuális állapota. Ne feledjük, hogy a kezdőértékét az init/1 visszahívásban állítottuk be.A handle_call/3 visszahívás általában egy 3-as tuple-t ad vissza: {:reply, reply, state}. Ez azt jelenti, hogy a tuple második eleme, a reply, amely bármilyen típusú lehet, visszaküldésre kerül 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 névről 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-vel küldhetsz egy szerverfolyamatnak. Az argumentumai megegyeznek a GenServer.call/2 argumentumaival.
A handle_cast/2 visszahívás felelős ezen üzenetek kezeléséért. Két argumentumot kap, a message-et é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 általában egy 2-es tuple-t ad vissza: {:noreply, state}.
Vannak ennél haladóbb lehetőségek is, amelyeket most nem tárgyalunk.
Hogy névről megjegyezd, mit csinál ez a visszahívás, jusson eszedbe, hogy a „cast” szó „dobni”-t is jelent.
Ha egy üzenetet palackba zárva a tengerbe dobsz, nem vársz azonnali válaszra, sőt, talán soha nem is kapsz.
call-t vagy a cast-ot használjam?Szinte mindig használd a call-t, még akkor is, ha a klienskódodnak nincs szüksége a szerver válaszára.
A call használata megvárja a választ, ami egy backpressure-mechanizmusként szolgál (megakadályozza, hogy a kliensek egyszerre túl sok üzenetet küldjenek). A szerver válaszának megérkezése az egyetlen módja annak, hogy biztos lehess abban, hogy a szerver megkapta és kezelte a kliens üzenetét.
handle_info/2Az üzenetek a GenServer.call/2 vagy a GenServer.cast/2 meghívásán kívül más módon is eljuthatnak a szerver postafiókjába, például a sima send/2 függvény meghívásával.
Az ilyen üzenetek kezeléséhez 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 egy mindent elkapó alapértelmezett implementációt biztosí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 szerepeljen benne a saját, mindent elkapó implementációd is. Ha elfelejted, a szerver összeomlik, ha nem várt üzenetet kap.
A fent leírt négy visszahívás visszatérési értéke egy további tuple-elemmel, egy időtúllépéssel egészíthető ki. Például ahelyett, hogy az init/1-ből {:ok, state}-et adnál vissza, adj vissza {:ok, state, timeout}-ot.
Az időtúllépéssel észlelhető, ha egy adott ideig nem érkezik üzenet a postafiókba. Ha a szerver egy időtúllépést ad vissza valamelyik visszahívásából, és eltelik a megadott ezredmásodperc anélkül, hogy üzenet érkezne, akkor a handle_info/2 hívódik meg :timeout első argumentummal.
Az alap Take-A-Number gép nagyon jól fogyott, de néhány felhasználó panaszkodott, hogy a piacon elérhető többi modellhez képest hiányoznak róla a fejlettebb funkciók.
A gyártó meghallgatta a felhasználói visszajelzéseket, és úgy döntött, hogy kiad egy deluxe modellt több funkcióval, neked pedig ismét az volt a feladatod, hogy megírd a gép szoftverét.
A deluxe modell új funkciói közé tartozik:
A gép üzleti logikáját már megírta a kollégád, és a TakeANumberDeluxe.State modulban található. Most az a feladatod, hogy becsomagold egy GenServer-be.
Használd a GenServer viselkedést a TakeANumberDeluxe modulban.
Valósítsd meg a start_link/1 függvényt és a szükséges GenServer callbacket.
A start_link/1 függvénynek átadott argumentum egy kulcsszólista. Tartalmazza a :min_number és a :max_number kulcsot. Az ezekhez a kulcsokhoz tartozó értékeket át kell adni a TakeANumberDeluxe.State.new/2 függvénynek.
Ha a TakeANumberDeluxe.State.new/2 egy {:ok, state} tuple-t ad vissza, a gépnek el kell indulnia, és a visszaadott állapotot kell használnia saját állapotaként. Ha viszont egy {:error, error} tuple-t ad vissza, a gépnek le kell állnia, és a visszaadott hibát kell megadnia a leállás okaként.
TakeANumberDeluxe.start_link(min_number: 1, max_number: 9)
# => {:ok, #PID<0.174.0>}
TakeANumberDeluxe.start_link(min_number: 9, max_number: 1)
# => {:error, :invalid_configuration}
Talán észrevetted, hogy a TakeANumberDeluxe.State.new/2 függvény egy opcionális harmadik argumentumot is fogad, az auto_shutdown_timeout értéket. Ezt a feladat utolsó lépésében fogjuk használni.
Valósítsd meg a report_state/1 függvényt és a szükséges GenServer callbacket. A gépnek a hívónak az aktuális állapotával kell válaszolnia.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.report_state(machine)
# => %TakeANumberDeluxe.State{
# max_number: 10,
# min_number: 1,
# queue: %TakeANumberDeluxe.Queue{in: [], out: []},
# auto_shutdown_timeout: :infinity,
# }
Valósítsd meg a queue_new_number/1 függvényt és a szükséges GenServer callbacket.
Hívja meg a TakeANumberDeluxe.State.queue_new_number/1 függvényt a gép aktuális állapotával.
Ha a TakeANumberDeluxe.State.queue_new_number/1 egy {:ok, new_number, new_state} tuple-t ad vissza, a gépnek a hívónak kell válaszolnia az új számmal, és az új állapotot kell beállítania saját állapotaként. Ha viszont egy {:error, error} tuple-t ad vissza, a gépnek a hívónak a hibával kell válaszolnia, és nem szabad megváltoztatnia az állapotát.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 2)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 2}
TakeANumberDeluxe.queue_new_number(machine)
# => {:error, :all_possible_numbers_are_in_use}
Valósítsd meg a serve_next_queued_number/2 függvényt és a szükséges GenServer callbacket.
Hívja meg a TakeANumberDeluxe.State.serve_next_queued_number/2 függvényt a gép aktuális állapotával és a második, opcionális argumentumával, a priority_number-rel.
Ha a TakeANumberDeluxe.State.serve_next_queued_number/2 egy {:ok, next_number, new_state} tuple-t ad vissza, a gépnek a hívónak kell válaszolnia a következő számmal, és az új állapotot kell beállítania saját állapotaként. Ha viszont egy {:error, error} tuple-t ad vissza, a gépnek a hívónak a hibával kell válaszolnia, és nem szabad megváltoztatnia az állapotát.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:error, :empty_queue}
Valósítsd meg a reset_state/1 függvényt és a szükséges GenServer callbacket.
Hívja meg a TakeANumberDeluxe.State.new/2 függvényt, hogy új állapotot hozzon létre az aktuális állapot min_number és max_number értékével. A gépnek be kell állítania az új állapotot saját állapotaként. Nem szabad válaszolnia a hívónak.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.reset_state(machine)
# => :ok
Módosítsd a gép indítását. Olvassa ki az init_arg-ként átadott kulcsszólista :auto_shutdown_timeout kulcsa alatti értéket, és adja át harmadik argumentumként a TakeANumberDeluxe.State.new/3 függvénynek. Ha a :auto_shutdown_timeout nincs megadva, használd a :infinity alapértelmezett értéket.
Módosítsd úgy a gép állapotának visszaállítását, hogy az auto_shutdown_timeout értéket is átadja a TakeANumberDeluxe.State.new/3 függvénynek.
Módosítsd az összes megvalósított callback visszatérési értékét (init/1 és az összes handle_* callback), hogy beállítsanak egy timeoutot. Használd az aktuális gép állapotában a :auto_shutdown_timeout kulcs alatti értéket. Az init/1 {:stop, reason} visszatérési értékéhez ne add hozzá a timeoutot: a timeoutok csak azután érvényesek, miután a szerver elindította a fogadó ciklusát.
Valósíts meg egy GenServer callbacket, amely kezeli a :timeout üzenetet, amit akkor kap a gép, ha a megadott timeouton belül nem érkezik más üzenet. A folyamatot :normal okkal kell kiléptetnie.
Arra is figyelj, hogy a váratlan üzeneteket is kezeld, egyszerűen hagyd figyelmen kívül őket.
{:ok, machine} =
TakeANumberDeluxe.start_link(
min_number: 1,
max_number: 10,
auto_shutdown_timeout: :timer.hours(2)
)
# after 3 hours...
TakeANumberDeluxe.queue_new_number(machine)
# => ** (exit) exited in: GenServer.call(#PID<0.171.0>, :queue_new_number, 5000)
# ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
# (elixir 1.13.0) lib/gen_server.ex:1030: GenServer.call/3
Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) Elixir nyelvet 58 fogalom168 feladat segítségével, valódi emberi mentorálással, mindez ingyen.