Cuando trabajamos con procesos en Elixir, pronto nos damos cuenta de que manejar todo a mano con receive, send, spawn y demás puede ser tedioso y propenso a errores. Por ejemplo, si tenemos una calculadora distribuida que lleva un contador de operaciones para facturar, debemos encargarnos manualmente de incrementar ese contador, gestionar las llamadas recursivas para seguir recibiendo mensajes y evitar que el proceso se quede bloqueado esperando mensajes que nunca llegan. Todo esto hace que el código sea más complejo y menos fiable.
Para facilitarnos la vida, Elixir nos ofrece módulos de alto nivel que envuelven la gestión de procesos y nos permiten trabajar con ellos de forma más segura y sencilla. Entre estos, los GenServers son una pieza fundamental. Un GenServer, o servidor genérico, es un proceso que actúa como un servidor al que podemos enviarle peticiones y recibir respuestas, muy parecido a cómo funcionan los servidores web. Esto nos permite delegar operaciones, como cálculos, a un proceso especializado que mantiene su propio estado interno.
Para crear un GenServer, definimos un módulo que use GenServer. Esto importa automáticamente un comportamiento que nos obliga a implementar ciertas funciones con nombres específicos para que Elixir sepa cómo interactuar con nuestro proceso. La función más importante que debemos definir es init, que se ejecuta cuando arrancamos el GenServer. Esta función recibe un parámetro inicial y debe devolver una tupla con :ok y el estado inicial del servidor. Este estado es persistente y se pasa a las funciones que gestionan los mensajes, permitiéndonos mantener y modificar datos internos como el contador de operaciones.
Por ejemplo, podemos iniciar un GenServer con un estado inicial que sea un mapa con un contador a cero:
defmodule GColk do
use GenServer
def init(param) do
IO.puts("Inicio GenServer GColk")
IO.inspect(param)
{:ok, %{contador: 0}}
end
end
Para arrancar este GenServer, usamos GenServer.start_link/2, pasando el módulo y el parámetro inicial:
{:ok, pid} = GenServer.start_link(GColk, {:hola, :buenas})
Una vez que el GenServer está en marcha, podemos enviarle mensajes para que los gestione. La función handle_info/2 es la encargada de recibir mensajes enviados con send/2. Esta función recibe el mensaje y el estado actual, y debe devolver una tupla con :noreply y el nuevo estado. Si no queremos modificar el estado, simplemente devolvemos el mismo.
def handle_info(msg, state) do
IO.puts("Me mandan mensaje #{inspect(msg)}")
{:noreply, state}
end
Cada vez que llega un mensaje, handle_info se ejecuta con el estado actualizado, lo que nos permite mantener un estado mutable de forma segura y controlada. Además, Elixir nos permite recargar el código de un módulo en caliente con la función r, lo que actualiza todos los GenServers que usan ese módulo sin necesidad de reiniciar el sistema.
Aunque handle_info es útil para mensajes enviados con send, no es la forma más habitual de interactuar con un GenServer. Normalmente, usaremos las funciones call y cast que proporcionan una interfaz más estructurada para enviar mensajes sincrónicos y asincrónicos respectivamente, y que nos permiten manejar respuestas de forma más clara. Pero eso lo veremos en detalle más adelante.
Con GenServers, podemos construir procesos que gestionan su propio estado y responden a mensajes de forma ordenada y segura, evitando tener que lidiar con los detalles bajos de receive y send. Esto hace que trabajar con concurrencia en Elixir sea mucho más accesible y robusto.