GenServer (generischer Server) ist ein Verhalten, das die üblichen Client-Server-Interaktionen zwischen Elixir-Prozessen abstrahiert.
Erinnerst du dich noch an die receive-Schleife aus der Lektion über Prozesse? Das GenServer-Verhalten bietet Abstraktionen, um solche Schleifen zu implementieren und um Nachrichten mit einem Prozess auszutauschen, der eine solche Schleife ausführt. Damit ist es einfacher, den Zustand zu halten und asynchronen Code auszuführen.
Sei gewarnt: Der Name GenServer ist mehrdeutig belegt. Er beschreibt auch ein Modul, das das GenServer-Verhalten nutzt, sowie einen Prozess, der aus einem Modul gestartet wurde, das das GenServer-Verhalten nutzt.
Das GenServer-Verhalten definiert einen erforderlichen Callback, init/1, und einige interessante optionale Callbacks: handle_call/3, handle_cast/2 und handle_info/3. Die Clients, die einen GenServer verwenden, sollen diese Callbacks nicht direkt aufrufen. Stattdessen bietet das GenServer-Modul Funktionen, mit denen Clients mit einem GenServer-Prozess kommunizieren können.
Häufig definiert ein einzelnes Modul sowohl eine Client-API, also eine Reihe von Funktionen, die andere Teile deiner Elixir-App aufrufen können, um mit diesem GenServer-Prozess zu kommunizieren, als auch Server-Callback-Implementierungen, die die Logik dieses GenServer enthalten.
Schauen wir uns zuerst ein einfaches Beispiel für einen GenServer an und lernen dann, was die einzelnen Callbacks bedeuten.
Das ist ein Beispielserver, der auf die wiederholten Fragen nerviger Mitfahrer auf einer langen Autofahrt reagieren kann, genauer gesagt auf die Frage: „Sind wir schon da?“. Er zählt mit, wie oft diese Frage gestellt wurde, und gibt zunehmend genervtere Antworten.
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/1Ein Server kann gestartet werden, indem du GenServer.start/3 oder GenServer.start_link/3 aufrufst. Den Unterschied zwischen diesen Funktionen haben wir im Links-Konzept kennengelernt.
Diese beiden Funktionen:
GenServer-Verhalten implementiert.init_arg. Wie der Name vermuten lässt, wird dieses Argument an den init/1-Callback übergeben.Wenn du einen Server mit GenServer.start/3 oder GenServer.start_link/3 startest, wird der init/1-Callback blockierend aufgerufen. Der Rückgabewert von init/1 entscheidet, ob der Server erfolgreich gestartet werden kann.
Der init/1-Callback gibt üblicherweise einen dieser Werte zurück:
{:ok, state}. Der Server startet seine receive-Schleife und verwendet state als Anfangszustand. state kann einen beliebigen Typ haben.{:stop, reason}. reason kann einen beliebigen Typ haben. Der Server startet seine receive-Schleife nicht. Der Prozess beendet sich mit dem angegebenen Grund.Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.
Wenn die receive-Schleife des Servers startet, geben die Funktionen GenServer.start/3 und GenServer.start_link/3 ein Tupel {:ok, pid} zurück. Andernfalls geben sie {:error, reason} zurück.
handle_call/3Eine Nachricht, die eine Antwort erfordert, kann mit GenServer.call/2 an einen Serverprozess gesendet werden. Diese Funktion erwartet als erstes Argument die pid eines laufenden Serverprozesses und als zweites Argument die Nachricht. Die Nachricht kann einen beliebigen Typ haben.
Der handle_call/3-Callback ist dafür zuständig, synchrone Nachrichten zu verarbeiten und zu beantworten. Er erhält drei Argumente:
message – der Wert, der als zweites Argument an GenServer.call/2 übergeben wurde.from – die pid des Prozesses, der GenServer.call/2 aufruft. Meistens kannst du dieses Argument ignorieren.state – der aktuelle Zustand des Servers. Denk daran, dass sein Anfangswert im init/1-Callback gesetzt wurde.Der handle_call/3-Callback gibt üblicherweise ein 3-Tupel {:reply, reply, state} zurück. Das bedeutet, dass das zweite Element des Tupels, ein reply beliebigen Typs, an den Aufrufer zurückgeschickt wird. Das dritte Element des Tupels, state, ist der neue Zustand des Servers nach der Verarbeitung dieser Nachricht.
Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.
Um dir anhand des Namens zu merken, was dieser Callback tut, stell dir vor, du rufst jemanden am Telefon an.
Wenn diese Person erreichbar ist, bekommst du sofort eine Antwort (synchron).
handle_cast/2Eine Nachricht, die keine Antwort erfordert, kann mit GenServer.cast/2 an einen Serverprozess gesendet werden. Ihre Argumente sind dieselben wie bei GenServer.call/2.
Der handle_cast/2-Callback ist dafür zuständig, diese Nachrichten zu verarbeiten. Er erhält zwei Argumente, message und state, dieselben wie beim handle_call/3-Callback (außer from).
Der handle_cast/2-Callback gibt üblicherweise ein 2-Tupel {:noreply, state} zurück.
Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.
Um dir anhand des Namens zu merken, was dieser Callback tut, denk daran, dass „to cast“ auch „werfen“ bedeutet.
Wenn du eine Flaschenpost ins Meer wirfst, erwartest du nicht sofort eine Antwort, vielleicht nie.
call oder cast verwenden?Verwende fast immer call, auch wenn dein Client-Code die Antwort des Servers nicht braucht.
Bei call wartest du auf die Antwort, was als Backpressure-Mechanismus dient (damit Clients nicht zu viele Nachrichten auf einmal senden). Eine Antwort vom Server zu erhalten ist außerdem die einzige Möglichkeit, sicher zu sein, dass der Server die Nachricht des Clients empfangen und verarbeitet hat.
handle_info/2Nachrichten können auch auf anderem Weg als über GenServer.call/2 oder GenServer.cast/2 im Posteingang des Servers landen, zum Beispiel durch einen direkten Aufruf der Funktion send/2.
Um solche Nachrichten zu verarbeiten, verwendest du den handle_info/2-Callback. Dieser Callback funktioniert genauso wie handle_cast/2.
Das GenServer-Verhalten bietet eine Catch-all-Implementierung von handle_info/2, die Fehler über unerwartete Nachrichten protokolliert. Wenn du diese Standardimplementierung überschreibst, achte darauf, immer eine eigene Catch-all-Implementierung einzufügen. Wenn du das vergisst, stürzt der Server ab, sobald er eine unerwartete Nachricht erhält.
Der Rückgabewert jedes der vier oben beschriebenen Callbacks kann um ein weiteres Tupel-Element erweitert werden, ein Timeout. Statt zum Beispiel aus init/1 ein {:ok, state} zurückzugeben, gibst du {:ok, state, timeout} zurück.
Das Timeout kannst du nutzen, um festzustellen, dass über einen bestimmten Zeitraum keine Nachrichten im Postfach eingehen. Wenn der Server aus einem seiner Callbacks ein Timeout zurückgibt und die angegebene Anzahl Millisekunden verstrichen ist, ohne dass eine Nachricht eintrifft, wird handle_info/2 mit :timeout als erstem Argument aufgerufen.