Cuando nos adentramos en el mundo de Elixir y OTP, uno de los conceptos fundamentales que debemos comprender son los procesos. En Elixir, un proceso no es lo mismo que un proceso del sistema operativo; más bien, es una hebra de ejecución independiente que corre dentro de la máquina virtual de Erlang, la base de OTP. Estos procesos nos permiten manejar la concurrencia de forma eficiente y segura, y para ello contamos con mecanismos como los enlaces y los monitores. Los enlaces asocian dos procesos de tal manera que si uno falla, el otro también se detiene, mientras que los monitores nos notifican cuando un proceso termina o falla, sin detenernos automáticamente. Esta filosofía de dejar que crasée nos ayuda a construir sistemas robustos, donde los errores se detectan y gestionan mediante estructuras superiores que pueden reiniciar procesos automáticamente.
Para identificar y gestionar procesos, podemos asignarles nombres mediante registries, lo que facilita su localización y evita tener que referirnos a ellos solo por su identificador numérico. Esto es especialmente útil cuando queremos implementar procesos globales o únicos, como singletons, dentro de nuestra aplicación.
En cuanto a la creación de procesos, Elixir nos ofrece primitivas como spawn, spawn_link y spawn_monitor. Estas funciones nos permiten lanzar procesos simples, procesos enlazados o procesos monitorizados, respectivamente. Además, existen variantes que aceptan una única función como parámetro, lo que simplifica la creación de procesos que ejecutan una función específica sin necesidad de especificar módulo, función y argumentos por separado.
Para observar y depurar procesos, herramientas como Observer son esenciales. Nos permiten visualizar gráficamente la lista de procesos activos, sus comunicaciones y realizar trazas para entender mejor el comportamiento de nuestra aplicación. También podemos obtener información programática sobre procesos mediante funciones como Process.alive? o Process.info.
La comunicación entre procesos se realiza mediante el envío y recepción de mensajes. Las primitivas send y receive son la base para esta interacción. Además, existen funciones más avanzadas como Process.send/3, que nos permiten controlar detalles del envío, y Process.send_after/3, que programa el envío de un mensaje tras un retraso determinado. Esta última es especialmente útil para implementar temporizadores o relojes internos en nuestros procesos.
Aunque trabajar directamente con estas primitivas es posible, suele ser tedioso y propenso a errores. Por eso, OTP nos ofrece abstracciones como GenServer, que simplifican la gestión de procesos con estado y la comunicación mediante callbacks bien definidos. Un GenServer implementa funciones como init para la inicialización, terminate para la limpieza, y maneja llamadas síncronas (handle_call), llamadas asíncronas (handle_cast) y mensajes generales (handle_info). Estas funciones deben devolver tuplas con instrucciones claras para OTP, como {:reply, response, state} o {:noreply, state}, y pueden incluir timeouts para controlar la espera de respuestas.
Para iniciar un GenServer, utilizamos funciones como start o start_link, siendo esta última la más común, ya que enlaza el proceso con el que lo lanza, facilitando la supervisión y el control de fallos. Es habitual definir una función start_link/1 en nuestros módulos para encapsular la lógica de inicio y facilitar su integración con supervisores.
Los supervisores son otro pilar fundamental de OTP. Se encargan de gestionar la vida de otros procesos, llamados hijos, que pueden ser GenServers, otros supervisores o estructuras como agentes y tareas. La configuración de un supervisor se basa en una lista de child_specs, que define qué procesos debe lanzar y cómo debe hacerlo. Las estrategias de supervisión determinan cómo reaccionar ante fallos: one_for_one reinicia solo el proceso que falló, one_for_all reinicia todos los procesos hijos si uno falla, y rest_for_one reinicia el proceso fallido y todos los que fueron iniciados después de él.
Para lanzar supervisores, usamos funciones como start_link/2 o start_link/3, dependiendo del contexto. El supervisor principal de una aplicación suele iniciarse con start_link/2, mientras que los supervisores secundarios o hijos se inician con start_link/3, permitiendo desacoplar la configuración de la lógica de inicio.
Con estas herramientas, podemos construir sistemas concurrentes complejos y robustos. Por ejemplo, en aplicaciones que manejan conexiones de red, como servidores web, podemos utilizar GenServers para gestionar sockets TCP y supervisores para controlar la vida de estos procesos, asegurando que si alguno falla, se reinicie automáticamente sin afectar al resto del sistema. De esta forma, encapsulamos la complejidad de la concurrencia y la gestión de errores, ofreciendo una interfaz sencilla y confiable para el resto de nuestra aplicación.