Cómo enviar mensajes con un GenServer

Siguiendo en el tour de los GenServer, las funciones más importantes de un GenServer son, sobre todo, handle_call; y también, handle_cast. Usaremos handle_call para gestionar llamadas síncronas por parte de los clientes que se conecten a nuestro servidor; y handle_cast para las llamadas asíncronas. 00:00 impl handle_call 06:05 GenServer.call 08:53 Timeouts 13:10 impl handle_cast 15:23 handle_info vs handle_cast 15:40 GenServer.cast 17:22 Envolviendo calls y casts

En el mundo de Elixir, los GenServers son una herramienta fundamental para construir aplicaciones concurrentes y distribuidas. Cuando trabajamos con ellos, es crucial entender cómo enviar y manejar mensajes de manera eficiente, y para ello contamos con distintas funciones que nos permiten controlar la comunicación entre procesos.

Una función que destaca por su utilidad es handle_call. Esta función, con aridad 3, es la encargada de gestionar mensajes que requieren una respuesta. A diferencia de handle_info, que solo recibe el mensaje sin información sobre quién lo envió, handle_call nos proporciona el mensaje, el PID del proceso que lo envió y el estado actual del GenServer. Esto nos permite implementar una arquitectura cliente-servidor real, donde el proceso cliente puede enviar una solicitud y esperar una respuesta concreta, como si estuviera llamando a una función en otro proceso.

Por ejemplo, imaginemos que queremos crear una calculadora distribuida. Podemos definir mensajes que representen operaciones como suma, resta o multiplicación, y manejar cada uno en handle_call mediante pattern matching. La función debe devolver una tupla que comienza con :reply, seguida del resultado que queremos enviar de vuelta al cliente, y el nuevo estado del GenServer. Así, cuando el cliente use GenServer.call, recibirá la respuesta directamente, sin necesidad de manejar manualmente los mensajes.

def handle_call({:add, a, b}, _from, state) do
  result = a + b
  {:reply, result, state}
end

Al enviar mensajes con GenServer.call, podemos también especificar un tiempo máximo de espera para la respuesta. Por defecto, si el GenServer no responde en cinco segundos, la llamada falla, evitando que el proceso cliente quede bloqueado indefinidamente. Si necesitamos más tiempo, podemos pasar un tercer parámetro con el timeout deseado, o incluso usar :infinity para esperar indefinidamente.

Para ilustrar esto, podemos hacer que handle_call espere un tiempo antes de responder usando Process.sleep:

def handle_call({:add, a, b}, _from, state) do
  Process.sleep(2000)  # Espera 2 segundos
  result = a + b
  {:reply, result, state}
end

Si el tiempo de espera supera el timeout configurado en la llamada, el cliente recibirá un error, pero el GenServer seguirá funcionando normalmente.

Por otro lado, cuando queremos enviar mensajes sin esperar respuesta, utilizamos handle_cast. Esta función, con aridad 2, recibe el mensaje y el estado, y no devuelve una respuesta al proceso que envió el mensaje. Es útil para operaciones que no requieren confirmación inmediata, como reiniciar un contador o actualizar un estado interno.

Un ejemplo sencillo de handle_cast para reiniciar un contador sería:

def handle_cast(:reset, _state) do
  IO.puts("Reset del contador")
  {:noreply, 0}
end

Al enviar un mensaje con GenServer.cast, el proceso cliente continúa su ejecución sin esperar nada, y el GenServer procesa el mensaje en segundo plano.

Es importante distinguir entre handle_info y handle_cast. Aunque ambos manejan mensajes que no esperan respuesta, handle_info se utiliza para mensajes enviados con send y suele reservarse para eventos del sistema o mensajes de monitoreo, mientras que handle_cast es la forma estándar de enviar mensajes asíncronos a un GenServer.

Para facilitar el uso de GenServers, es común envolver las llamadas a GenServer.call y GenServer.cast en funciones con nombres descriptivos, evitando tener que manipular directamente las tuplas de mensajes. Por ejemplo, podemos definir una función add que envíe el mensaje adecuado al GenServer y devuelva el resultado:

def add(pid, a, b) do
  GenServer.call(pid, {:add, a, b})
end

De esta manera, la interfaz para interactuar con el GenServer es mucho más limpia y fácil de usar.

En resumen, al trabajar con GenServers, handle_call es nuestra herramienta para solicitudes que requieren respuesta, con la ventaja de conocer quién envió el mensaje y poder gestionar timeouts. handle_cast nos permite enviar mensajes sin esperar respuesta, ideal para operaciones asíncronas. Y handle_info queda reservado para mensajes externos o de sistema que no encajan en las otras dos categorías. Con estas funciones, podemos construir arquitecturas cliente-servidor robustas y eficientes en Elixir.

Lista de reproducción
  1. 1
    Cómo crear procesos
    11 minutos
  2. 2
    Cómo pasar mensajes entre procesos
    13 minutos
  3. 3
    Diccionario de un proceso y mantener un estado
    15 minutos
  4. 4
    Cómo enlazar procesos para detectar fallos
    13 minutos
  5. 5
    Cómo monitorizar procesos
    10 minutos
  6. 6
    ¿Qué es un GenServer?
    15 minutos
  7. 7
    Cómo enviar mensajes con un GenServer
    19 minutos
  8. 8
    Control de errores y gestión de un GenServer
    19 minutos
  9. 9
    Cómo renombrar procesos
    11 minutos
  10. 10
    Cómo crear un Supervisor Tree
    17 minutos
  11. 11
    Estrategias para trabajar con Supervisor
    18 minutos
  12. 12
    Estrategias para crear un Supervisor
    11 minutos
  13. 13
    Resumen sobre procesos OTP
    12 minutos
  14. 14
    Cómo usar Application
    12 minutos
  15. 15
    Ejemplo de Application con hijos
    14 minutos