Kurzusok
/
Elixir
Elixir
/
Feladatok
/
Húzz egy számot Deluxe
Húzz egy számot Deluxe

Húzz egy számot Deluxe

Tanulófeladat

Bevezetés

GenServer

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.

Note

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.

Példa

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

Visszahívások

init/1

Egy 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:

  • Első argumentumként egy olyan modult fogad, amely implementálja a GenServer viselkedést.
  • Második argumentumként bármit elfogad, amit init_arg-nak nevezünk. Ahogy a neve is sugallja, ezt az argumentumot kapja meg az init/1 visszahívás.
  • Elfogad egy opcionális harmadik argumentumot is, amely haladó beállításokat tartalmaz a folyamat futtatásához; ezeket most nem tárgyaljuk.

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/3

Egy 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:

  1. message - a GenServer.call/2 második argumentumaként átadott érték.
  2. 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.
  3. 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.

Note

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/2

Egy 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.

Note

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.

A 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/2

Az ü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.

Időtúllépések

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.

Utasítások

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:

  • Nyomon követi az éppen sorban álló számokat.
  • Beállítható a legkisebb és a legnagyobb szám. Így több deluxe Take-A-Number gép is használható ugyanabban az intézményben arra, hogy a különböző részlegekhez sorba állítsa az ügyfeleket, és a számok tartományával meg lehessen különböztetni a részlegeket.
  • Egyes számok átugorhatják a sort, hogy elsőbbségi kiszolgálást kapjanak a várandós nők és az idősek.
  • Automatikus leállítás, hogy a gép véletlenül se maradjon bekapcsolva az egész hétvégére, és ne pazarolja az energiát.

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.

1. Indítsd el a gépet

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.

2. Jelentsd a gép állapotát

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,
#    }

3. Állíts sorba új számokat

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}

4. Szolgáld ki a következő sorban álló számot

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}

5. Állítsd vissza az állapotot

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

6. Valósítsd meg az automatikus leállítást

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
Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
Elixir Exercism

Készen állsz elkezdeni a(z) Húzz egy számot Deluxe feladatot?

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.