GenServer (servidor genérico) es un comportamiento que abstrae las interacciones cliente-servidor comunes entre procesos de Elixir.
¿Recuerdas el bucle de recepción de cuando aprendimos sobre procesos? El comportamiento GenServer ofrece abstracciones para implementar esos bucles y para intercambiar mensajes con un proceso que ejecuta uno de ellos. Hace más fácil mantener el estado y ejecutar código asíncrono.
Ten cuidado, 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. Se supone que los clientes que usan un GenServer no llaman 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 solo módulo define tanto una API de cliente, un conjunto de funciones que otras partes de tu aplicación de Elixir pueden llamar para comunicarse con este proceso GenServer, como implementaciones de callbacks del servidor, que contienen la lógica de este GenServer.
Veamos primero un ejemplo sencillo de un GenServer y después aprendamos qué significa cada callback.
Este es un ejemplo de servidor que puede responder a los repetitivos interrogatorios de pasajeros molestos durante un largo viaje por carretera, más exactamente a la pregunta: «¿ya llegamos?». 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 links.
Esas dos funciones:
GenServer como primer argumento.init_arg. Como su nombre sugiere, este argumento se pasa al callback init/1.Iniciar un servidor llamando a GenServer.start/3 o GenServer.start_link/3 invoca el callback init/1 de forma bloqueante. El valor de retorno de init/1 determina si el servidor se puede iniciar correctamente.
El callback init/1 normalmente devuelve 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 cubriremos 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}. De lo contrario, devuelven {:error, reason}
handle_call/3Se puede enviar un mensaje que requiere una respuesta a un proceso servidor con GenServer.call/2. Esta función espera el pid de un proceso servidor en ejecución como primer argumento, y el mensaje como segundo argumento. El mensaje puede ser de cualquier tipo.
El callback handle_call/3 se encarga de manejar y responder a los mensajes sincrónicos. Recibe tres argumentos:
message: el valor pasado 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 normalmente devuelve 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 después de manejar este mensaje.
También hay posibilidades más avanzadas que no cubriremos ahora.
Para memorizar lo que 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 sincrónica).
handle_cast/2Se puede enviar un mensaje que no requiere una respuesta 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 manejar esos mensajes. Recibe dos argumentos, message y state, que son los mismos argumentos que en el callback handle_call/3 (excepto from).
El callback handle_cast/2 normalmente devuelve una tupla de 2 elementos, {:noreply, state}.
También hay posibilidades más avanzadas que no cubriremos ahora.
Para memorizar lo que 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 quizá nunca.
call o cast?Casi siempre usa call, aunque tu código de cliente no necesite 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 a la vez). Recibir una respuesta del servidor también es la única forma de asegurarte de que el servidor recibió y manejó el mensaje del cliente.
handle_info/2Los mensajes también pueden llegar al 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 manejar 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 abarca todos los casos y registra errores sobre mensajes inesperados. Si sobrescribes esa implementación predeterminada, asegúrate de incluir siempre tu propia implementación que abarque todos los casos. Si lo olvidas, el servidor fallará si recibe un mensaje inesperado.
El valor de retorno de cada uno de los cuatro callbacks descritos arriba se puede ampliar con un elemento más de 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 período específico. Si el servidor devuelve un tiempo de espera desde uno de sus callbacks, y ha transcurrido el número especificado de milisegundos sin que llegue ningún mensaje, se llama a handle_info/2 con :timeout como primer argumento.