Los procesos de Elixir están aislados y, de forma predeterminada, no comparten nada. Cuando un proceso hijo que no está enlazado falla, el proceso padre no se ve afectado.
parent_pid =
spawn(fn ->
spawn(fn -> raise "oops" end)
receive do
message -> IO.inspect(message, label: "received message")
end
end)
# => 20:03:08.405 [error] Process #PID<0.153.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
Process.alive?(parent_pid)
# => true
Este comportamiento se puede cambiar enlazando procesos entre sí. Si dos procesos están enlazados, una falla en uno de ellos se propagará al otro proceso. Los enlaces son bidireccionales.
parent_pid =
spawn(fn ->
spawn_link(fn -> raise "oops" end)
receive do
message ->
IO.inspect(message, label: "received message")
end
end)
# => 20:05:34.125 [error] Process #PID<0.171.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
Process.alive?(parent_pid)
# => false
Los procesos se pueden crear ya enlazados al proceso que los invoca usando spawn_link/1, que es una operación atómica, o se pueden enlazar más adelante con Process.link/1.
Puedes consultar qué procesos están enlazados actualmente a un proceso (y siguen en ejecución) con Process.info/1.
pid = spawn_link(fn -> :timer.sleep(50_000) end)
# => #PID<0.126.0>
self()
|> Process.info()
|> Keyword.get(:links)
# => [#PID<0.126.0>]
Enlazar procesos puede ser útil cuando haces trabajo en paralelo y cada parte del trabajo no debería continuar si otra parte no logra terminar.
El enlace también se puede usar para supervisar procesos. Si un proceso captura las salidas, no fallará cuando falle un proceso al que está enlazado. En su lugar, recibirá un mensaje sobre la falla. Esto le permite manejar la falla de forma ordenada, por ejemplo, reiniciando el proceso que falló.
Un proceso se puede configurar para capturar salidas llamando a Process.flag(:trap_exit, true). Ten en cuenta que Process.flag/2 devuelve el valor anterior de la bandera, no el nuevo.
El valor actual de la bandera se puede consultar con Process.info/1.
pid
|> Process.info()
|> Keyword.get(:trap_exit)
# => false
El mensaje que se enviará al proceso cuando falle un proceso enlazado coincidirá con el patrón {:EXIT, from, reason}, donde from es un PID. Si reason es algo distinto del átomo :normal, eso significa que el proceso falló o fue eliminado por la fuerza.
parent_pid =
spawn(fn ->
Process.flag(:trap_exit, true)
spawn_link(fn -> raise "oops" end)
receive do
message ->
IO.inspect(message, label: "received message")
# do something else,
# to demonstrate that the process stays alive
:timer.sleep(50_000)
end
end)
# => 20:05:34.125 [error] Process #PID<0.295.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
# received message: {:EXIT, #PID<0.295.0>,
# {%RuntimeError{message: "oops"},
# [{:erl_eval, :do_apply, 6, [file: 'erl_eval.erl', line: 678]}]}}
Process.alive?(parent_pid)
# => true
Ten en cuenta que capturar salidas hace que el proceso sea inmune a las fallas de todos sus procesos enlazados, y los mensajes de salida podrían acumularse en el buzón del proceso si no se leen. Por eso, deberías llevar un registro de todos los procesos enlazados a un proceso que captura salidas. Por lo general, los procesos que capturan salidas solo se encargan de supervisar otros procesos y no hacen nada más.
En la práctica, no implementarías tú mismo un proceso supervisor desde cero. Elixir ofrece una abstracción para este tipo de funcionalidad: el comportamiento Supervisor.
Otras abstracciones en torno a los procesos, como Agent, GenServer o Task, ofrecen dos formas de iniciarlos: start o start_link.