Tracks
/
Elixir
Elixir
/
Temario
/
GenServer
Ge

GenServer en Elixir

4 ejercicios

Acerca de GenServer

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.

Note

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.

Ejemplo

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

Callbacks

init/1

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

  • Aceptan un módulo que implementa el comportamiento GenServer como primer argumento.
  • Aceptan cualquier cosa como segundo argumento, llamado init_arg. Como su nombre sugiere, este argumento se pasa al callback init/1.
  • Aceptan un tercer argumento opcional con opciones avanzadas para ejecutar el proceso, que no cubriremos ahora.

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

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

  1. message: el valor pasado como segundo argumento a GenServer.call/2.
  2. from: el pid del proceso que llama a GenServer.call/2. La mayoría de las veces este argumento se puede ignorar.
  3. 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.

Note

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

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

Note

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.

¿Debería usar 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/2

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

Tiempos de espera

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.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende GenServer

La práctica está bloqueada

Desbloquea 2 ejercicios más para practicar GenServer