GenServer (servidor genérico) es un comportamiento que abstrae las interacciones habituales entre cliente y servidor que se dan entre procesos de Elixir.
¿Recuerdas el bucle de recepción de cuando aprendimos sobre los procesos? El comportamiento GenServer ofrece abstracciones para implementar ese tipo de bucles y para intercambiar mensajes con un proceso que ejecuta uno de ellos. Además, facilita mantener el estado y ejecutar código asíncrono.
Ten en cuenta que el nombre GenServer está sobrecargado: también se usa para describir un módulo que usa el comportamiento GenServer, así como un proceso que se inició desde un módulo que usa el comportamiento GenServer.
El comportamiento GenServer define un callback obligatorio, init/1, y algunos callbacks opcionales interesantes: handle_call/3, handle_cast/2 y handle_info/3. Los clientes que usan un GenServer no deben llamar a esos callbacks directamente. En su lugar, el módulo GenServer proporciona funciones que los clientes pueden usar para comunicarse con un proceso GenServer.
A menudo, un mismo módulo define tanto una API de cliente, un conjunto de funciones que otras partes de tu aplicación Elixir pueden llamar para comunicarse con este proceso GenServer, como las implementaciones de los callbacks del servidor, que contienen la lógica de este GenServer.
Vamos a ver primero un ejemplo sencillo de GenServer y después aprenderemos qué significa cada callback.
Este es un ejemplo de servidor que puede responder a las repetitivas preguntas de un pasajero pesado durante un largo viaje por carretera, más concretamente a la pregunta: «¿ya hemos llegado?». Lleva la cuenta de cuántas veces se ha hecho esa pregunta y devuelve respuestas cada vez más irritadas.
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/1Un servidor se puede iniciar llamando a GenServer.start/3 o GenServer.start_link/3. Aprendimos la diferencia entre esas funciones en el concepto de enlaces.
Esas dos funciones:
GenServer.init_arg. Como sugiere el nombre, este argumento se pasa al callback init/1.Iniciar un servidor llamando a GenServer.start/3 o GenServer.start_link/3 invocará el callback init/1 de forma bloqueante. El valor devuelto por init/1 determina si el servidor puede iniciarse correctamente.
El callback init/1 suele devolver uno de estos valores:
{:ok, state}. El servidor iniciará su bucle de recepción usando state como estado inicial. state puede ser de cualquier tipo.{:stop, reason}. reason puede ser de cualquier tipo. El servidor no iniciará su bucle de recepción. El proceso terminará con el motivo indicado.También hay posibilidades más avanzadas que no vamos a tratar ahora.
Si el bucle de recepción del servidor se inicia, las funciones GenServer.start/3 y GenServer.start_link/3 devuelven una tupla {:ok, pid}. En caso contrario, devuelven {:error, reason}
handle_call/3Un mensaje que requiere una respuesta se puede enviar a un proceso servidor con GenServer.call/2. Esta función espera como primer argumento el pid de un proceso servidor en ejecución, y como segundo argumento el mensaje. El mensaje puede ser de cualquier tipo.
El callback handle_call/3 se encarga de gestionar y responder a los mensajes síncronos. Recibe tres argumentos:
message: el valor que se pasa como segundo argumento a GenServer.call/2.from: el pid del proceso que llama a GenServer.call/2. La mayoría de las veces este argumento se puede ignorar.state: el estado actual del servidor. Recuerda que su valor inicial se estableció en el callback init/1.El callback handle_call/3 suele devolver una tupla de 3 elementos, {:reply, reply, state}. Esto significa que el segundo elemento de la tupla, un reply que puede ser de cualquier tipo, se enviará de vuelta a quien hizo la llamada. El tercer elemento de la tupla, state, es el nuevo estado del servidor tras gestionar este mensaje.
También hay posibilidades más avanzadas que no vamos a tratar ahora.
Para memorizar qué hace este callback por su nombre, piensa en «llamar» a alguien por teléfono.
Si esa persona está disponible, recibirás una respuesta de inmediato (de forma síncrona).
handle_cast/2Un mensaje que no requiere una respuesta se puede enviar a un proceso servidor con GenServer.cast/2. Sus argumentos son idénticos a los de GenServer.call/2.
El callback handle_cast/2 se encarga de gestionar esos mensajes. Recibe dos argumentos, message y state, que son los mismos que en el callback handle_call/3 (excepto from).
El callback handle_cast/2 suele devolver una tupla de 2 elementos, {:noreply, state}.
También hay posibilidades más avanzadas que no vamos a tratar ahora.
Para memorizar qué hace este callback por su nombre, recuerda que to cast también significa «lanzar».
Si lanzas un mensaje en una botella al mar, no esperas recibir una respuesta de inmediato, o puede que nunca la recibas.
call o cast?Casi siempre usa call, incluso si tu código de cliente no necesita la respuesta del servidor.
Usar call espera la respuesta, lo que sirve como mecanismo de contrapresión (para evitar que los clientes envíen demasiados mensajes de golpe). Recibir una respuesta del servidor es también la única forma de asegurarte de que el servidor recibió y gestionó el mensaje del cliente.
handle_info/2Los mensajes también pueden acabar en el buzón del servidor por medios distintos de llamar a GenServer.call/2 o GenServer.cast/2, por ejemplo llamando a la función send/2 directamente.
Para gestionar esos mensajes, usa el callback handle_info/2. Este callback funciona exactamente igual que handle_cast/2.
El comportamiento GenServer proporciona una implementación de handle_info/2 que captura todos los mensajes y registra errores sobre los mensajes inesperados. Si sobrescribes esa implementación por defecto, asegúrate de incluir siempre tu propia implementación que capture todos los mensajes. Si lo olvidas, el servidor fallará si recibe un mensaje inesperado.
El valor devuelto por cada uno de los cuatro callbacks descritos anteriormente se puede ampliar con un elemento más en la tupla, un tiempo de espera. Por ejemplo, en lugar de devolver {:ok, state} desde init/1, devuelve {:ok, state, timeout}.
El tiempo de espera se puede usar para detectar la ausencia de mensajes en el buzón durante un periodo concreto. Si el servidor devuelve un tiempo de espera desde uno de sus callbacks, y ha transcurrido el número de milisegundos indicado sin que llegue ningún mensaje, se llama a handle_info/2 con :timeout como primer argumento.