Déjalo fallar
Al igual que en cualquier otro lenguaje de programación, en Elixir las cosas pueden salir mal y puede haber errores de programación. Cuando un error de programación afecta a un proceso, todo ese proceso falla, salvo que se trate.
Sin embargo, resulta que en Elixir, al igual que en el resto del ecosistema Erlang, los procesos son tan ligeros que la solución aceptada es que cuando un proceso falla es mejor dejarlo caer y reiniciarlo, antes que tratar de recuperarlo y dejarlo en un estado inconsistente o tener que comunicar de vuelta con otros procesos que tengan que saber que se ha caído.
Por lo tanto, en Elixir la filosofía a menudo va a ser dejar que un proceso en caso de error se reinicie y se vuelva a lanzar. En otras palabras, el tratamiento del error no ocurrirá dentro del proceso que falla, sino fuera del mismo. Sin embargo, para poder implementar esto, una de las primeras cosas que necesitaremos es poder detectar correctamente cuándo alguno de los procesos que lancemos ha fallado, para poder tomar esa acción correctiva.
Qué ocurre cuando un proceso falla
Vamos a mostrar qué consecuencias tiene ver fallar un proceso. En primer lugar, imaginemos el siguiente módulo con código síncrono.
# Archivo: calc.exs
defmodule Calc do
def dividir do
value = 5 / 0
IO.inspect(value)
end
end
Calc.dividir
Si lo ejecutamos mediante elixir calc.exs, veremos un error aplastante, porque estamos intentando dividir por cero.
** (ArithmeticError) bad argument in arithmetic expression
calc.exs:3: Calc.dividir/0
calc.exs:8: (file)
Modificando el código para que se ejecute en otro proceso, vamos a quitar la llamada a Calc.dividir y cambiarlo por esto otro:
# Archivo: calc.exs
_ = spawn(Calc, :dividir, [])
receive do
x -> IO.inspect(x, label: "Recibido")
after 1_000 -> IO.puts("Timeout")
end
IO.puts("Sigo funcionando como si nada")
Si ahora volvemos a ejecutar esto con elixir calc.exs, veremos que como tal la propia salida de errores de Elixir admite que el proceso ha fallado, pero eso no impide que el resto del script continúe evaluándose como si nada:
07:41:39.190 [error] Process #PID<0.105.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
calc.exs:3: Calc.dividir/0
Timeout
Sigo funcionando como si nada
Dicho de otro modo, cuando un proceso falla en Elixir, el error no afecta a otros procesos por defecto. Si tratamos de repetir esto de forma interactiva utilizando IEx, nos encontraremos en una situación parecida. El proceso falla, y el error se reporta por salida de errores, pero como tal no afecta al funcionamiento de IEx.
Enlazando procesos con link
El primer paso hacia poder detectar cuando un proceso ha fallado es enlazarlo. Cuando dos procesos A y B están enlazados, que A aborte su ejecución notificará a B, y que B aborte su ejecución notificará a A. Un enlace es, por lo tanto, bidireccional.
La forma de enlazar dos procesos es mediante Process.link/1. Esta función tiene que ser invocada en uno de los dos procesos que queremos enlazar, y debe recibir como parámetro el otro proceso que se vaya a enlazar. Dicho de otro modo, llamar a Process.link(pid), enlazará los procesos self() y pid de forma bidireccional.
A tener en cuenta que pueden existir tantos enlaces en una máquina BEAM como se quiera, pero un par de procesos A y B sólo pueden tener un único enlace. Eso quiere decir que si llamas varias veces a Process.link() desde el mismo proceso y enlazando contra el mismo proceso, como no se pueden establecer más de un enlace entre A y B las sucesivas llamadas se van a ignorar.
Si conectamos dos procesos mediante link, el comportamiento por defecto será que si un proceso es abortado, el otro proceso del enlace interrumpirá inmediatamente su ejecución también por la misma razón.
En el caso del programa calc.exs, colocando el siguiente código tras la definición del módulo nos permite ver un ejemplo de uso de ese link:
# Archivo: calc.exs
pid = spawn(Calc, :dividir, [])
Process.link(pid)
receive do
x -> IO.inspect(x, label: "Recibido")
after 1_000 -> IO.puts("Timeout")
end
IO.puts("Sigo funcionando como si nada")
Ahora al lanzar el script con elixir calc.exs, obtenemos una salida diferente:
** (EXIT from #PID<0.98.0>) an exception was raised:
** (ArithmeticError) bad argument in arithmetic expression
calc.exs:3: Calc.dividir/0
Fíjate que ya no vemos los mensajes del receive ni los que vienen después, sino que el programa termina inmediatamente; esto ocurre porque, por defecto, al haber salido con error el proceso pid, el proceso self() también sale.
Veremos con el tiempo que para poder cambiar lo que hacemos con los procesos que fallan, deberemos usar un monitor. La idea de esto es que en vez de terminar la ejecución del programa, podamos recibir un mensaje más neutro que nos informe que el PID ha terminado para que podamos actuar de algún modo a nuestra manera.
spawn + link =spawn_link
Si quieres crear un proceso con el enlace ya puesto, puedes utilizar la función Kernel.spawn_link/3. Esta función es igual que Kernel.spawn/3, pero el proceso ya se crea con un enlace puesto con self() sin tener que hacerlo a mano.
spawn_link(Calc, :dividir, [])
receive do
x -> IO.inspect(x, label: "Recibido")
after 1_000 -> IO.puts("Timeout")
end
** (EXIT from #PID<0.98.0>) an exception was raised:
** (ArithmeticError) bad argument in arithmetic expression
calc.exs:3: Calc.dividir/0
Anular un enlace con unlink
Del mismo modo que Process.link/1 crea enlaces, Process.unlink/1 los destruye. Para usar esta función, le indicamos como parámetro el PID de un proceso que previamente haya sido conectado con el proceso self() desde donde se llame a esa función, y la consecuencia es que el enlace entre el proceso self() y el proceso dado como parámetro desaparece.
Cuando desaparece un enlace, dejamos de enterarnos y de fallar en caso de que se produzcan errores.
Sólo es posible eliminar un enlace entre dos procesos A y B si este enlace existía previamente. Llamar a la función Process.unlink/1 cuando no existía este enlace entre el proceso self() y el proceso dado como parámetro no mostrará errores visibles, pero tampoco hará nada. Llamar varias veces a Process.unlink/1 tampoco hará nada más allá de la primera invocación.
¿Esto se usa?
Es útil conocer las primitivas y las bases por las que se levanta el resto del sistema de concurrencia de Elixir, y esta es la razón por la que esta lección muestra cómo establecer y eliminar enlaces entre procesos.
La realidad es que generalmente en programación concurrente con Elixir vamos a preferir otro tipo de abstracciones fabricadas por encima de todo esto para no tener que programar los enlaces por nuestra cuenta. Los agentes, los GenServers... estas abstracciones hacen un mejor trabajo detectando errores que el que podamos hacer a mano.