GenServer (serveur générique) est un comportement qui abstrait les interactions client-serveur courantes entre processus Elixir.
Tu te souviens de la boucle de réception dont on a parlé quand on a découvert les processus ? Le comportement GenServer fournit des abstractions pour implémenter de telles boucles, et pour échanger des messages avec un processus qui exécute une telle boucle. Il facilite la conservation d'un état et l'exécution de code asynchrone.
Attention, le nom GenServer est chargé de sens. Il sert aussi à désigner un module qui utilise le comportement GenServer, ainsi qu'un processus démarré à partir d'un module qui utilise le comportement GenServer.
Le comportement GenServer définit un callback obligatoire, init/1, et quelques callbacks optionnels intéressants : handle_call/3, handle_cast/2 et handle_info/3. Les clients qui utilisent un GenServer ne sont pas censés appeler ces callbacks directement. À la place, le module GenServer fournit des fonctions que les clients peuvent utiliser pour communiquer avec un processus GenServer.
Souvent, un seul module définit à la fois une API client, un ensemble de fonctions que les autres parties de ton application Elixir peuvent appeler pour communiquer avec ce processus GenServer, et des implémentations de callbacks côté serveur, qui contiennent la logique de ce GenServer.
On va d'abord regarder un exemple simple de GenServer, puis on verra ce que fait chaque callback.
Voici un exemple de serveur capable de répondre aux questions répétitives de passagers agaçants lors d'un long trajet en voiture, plus précisément à la question : « est-ce qu'on est bientôt arrivés ? ». Il garde la trace du nombre de fois où cette question a été posée et renvoie des réponses de plus en plus agacées.
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/1On peut démarrer un serveur en appelant GenServer.start/3 ou GenServer.start_link/3. On a vu la différence entre ces deux fonctions dans le concept des liens.
Ces deux fonctions :
GenServer.init_arg. Comme son nom l'indique, cet argument est transmis au callback init/1.Appeler GenServer.start/3 ou GenServer.start_link/3 pour démarrer un serveur déclenche le callback init/1 de manière bloquante. La valeur de retour de init/1 détermine si le serveur peut démarrer correctement.
Le callback init/1 renvoie généralement l'une de ces valeurs :
{:ok, state}. Le serveur démarre sa boucle de réception en utilisant state comme état initial. state peut être de n'importe quel type.{:stop, reason}. reason peut être de n'importe quel type. Le serveur ne démarre pas sa boucle de réception. Le processus se termine avec la raison donnée.Il existe aussi des possibilités plus avancées que nous n'aborderons pas ici.
Si la boucle de réception du serveur démarre, les fonctions GenServer.start/3 et GenServer.start_link/3 renvoient un tuple {:ok, pid}. Sinon, elles renvoient {:error, reason}
handle_call/3On peut envoyer à un processus serveur un message qui attend une réponse avec GenServer.call/2. Cette fonction attend en premier argument le pid d'un processus serveur en cours d'exécution, et en deuxième argument le message. Le message peut être de n'importe quel type.
Le callback handle_call/3 se charge de traiter les messages synchrones et d'y répondre. Il reçoit trois arguments :
message : la valeur passée en deuxième argument à GenServer.call/2.from : le pid du processus qui appelle GenServer.call/2. La plupart du temps, cet argument peut être ignoré.state : l'état actuel du serveur. Rappelle-toi que sa valeur initiale a été définie dans le callback init/1.Le callback handle_call/3 renvoie généralement un tuple de 3 éléments, {:reply, reply, state}. Cela signifie que le deuxième élément du tuple, un reply qui peut être de n'importe quel type, est renvoyé à l'appelant. Le troisième élément du tuple, state, est le nouvel état du serveur après le traitement de ce message.
Il existe aussi des possibilités plus avancées que nous n'aborderons pas ici.
Pour retenir ce que fait ce callback à partir de son nom, pense au fait de « appeler » quelqu'un au téléphone.
Si cette personne est disponible, tu reçois une réponse immédiatement (de façon synchrone).
handle_cast/2On peut envoyer à un processus serveur un message qui n'attend pas de réponse avec GenServer.cast/2. Ses arguments sont identiques à ceux de GenServer.call/2.
Le callback handle_cast/2 se charge de traiter ces messages. Il reçoit deux arguments, message et state, qui sont les mêmes que dans le callback handle_call/3 (sauf from).
Le callback handle_cast/2 renvoie généralement un tuple de 2 éléments, {:noreply, state}.
Il existe aussi des possibilités plus avancées que nous n'aborderons pas ici.
Pour retenir ce que fait ce callback à partir de son nom, rappelle-toi que « to cast » signifie aussi « jeter ».
Si tu jettes un message dans une bouteille à la mer, tu ne t'attends pas à recevoir une réponse immédiatement, voire jamais.
call ou cast ?Utilise presque toujours call, même si ton code client n'a pas besoin de la réponse du serveur.
Utiliser call attend la réponse, ce qui sert de mécanisme de contre-pression (pour éviter que les clients n'envoient trop de messages d'un coup). Recevoir une réponse du serveur est aussi le seul moyen de s'assurer que le serveur a bien reçu et traité le message du client.
handle_info/2Des messages peuvent aussi arriver dans la boîte de réception du serveur par d'autres moyens que l'appel à GenServer.call/2 ou GenServer.cast/2, par exemple en appelant la simple fonction send/2.
Pour traiter ces messages, utilise le callback handle_info/2. Ce callback fonctionne exactement de la même façon que handle_cast/2.
Le comportement GenServer fournit une implémentation fourre-tout de handle_info/2, qui journalise les erreurs liées aux messages inattendus. Si tu redéfinis cette implémentation par défaut, veille à toujours inclure ta propre implémentation fourre-tout. Si tu oublies, le serveur plantera s'il reçoit un message inattendu.
La valeur de retour de chacun des quatre callbacks décrits ci-dessus peut être complétée par un élément de tuple supplémentaire, un délai d'attente. Par exemple, au lieu de renvoyer {:ok, state} depuis init/1, renvoie {:ok, state, timeout}.
Le délai d'attente peut servir à détecter l'absence de messages dans la boîte aux lettres pendant une période donnée. Si le serveur renvoie un délai d'attente depuis l'un de ses callbacks, et que le nombre de millisecondes spécifié s'est écoulé sans qu'aucun message n'arrive, handle_info/2 est appelé avec :timeout comme premier argument.