Translate

sábado, 1 de febrero de 2025

Concurrencia en Erlang parte 10


Ahora volvamos a los links y procesos que mueren. La propagación de errores entre procesos se realiza a través de un proceso similar al paso de mensajes, pero con un tipo especial de mensaje llamado señales o signals. Las señales de salida son mensajes "secretos" que actúan automáticamente sobre los procesos, matándolos en la acción.

Ya he mencionado muchas veces que para ser confiable, una aplicación necesita poder matar y reiniciar un proceso rápidamente. En este momento, los enlaces están bien para hacer la parte de matar. Lo que falta es el reinicio.

Para reiniciar un proceso, necesitamos una forma de saber primero que murió. Esto se puede hacer agregando una capa sobre los enlaces con un concepto llamado procesos del sistema. Los procesos del sistema son básicamente procesos normales, excepto que pueden convertir señales de salida en mensajes normales. Esto se hace llamando a process_flag(trap_exit, true) en un proceso en ejecución. Nada dice tanto como un ejemplo, así que lo usaremos. Simplemente volveré a hacer el ejemplo de la cadena con un proceso del sistema al principio:


1> process_flag(trap_exit, true).

true

2> spawn_link(fun() -> linkmon:chain(3) end).

<0.49.0>

3> receive X -> X end.

{'EXIT',<0.49.0>,"chain dies here"}


Ahora las cosas se ponen interesantes. Volviendo a nuestros dibujos, lo que sucede ahora es más bien así:


[shell] == [3] == [2] == [1] == [0]

[shell] == [3] == [2] == [1] == *dead*

[shell] == [3] == [2] == *dead*

[shell] == [3] == *dead*

[shell] <-- {'EXIT,Pid,"chain dies here"} -- *dead*

[shell] <-- still alive!


Y este es el mecanismo que permite reiniciar rápidamente los procesos. Al escribir programas que utilizan procesos del sistema, es fácil crear un proceso cuyo único papel sea comprobar si algo muere y luego reiniciarlo cuando falle.

Por ahora, quiero volver a las funciones de excepción que vimos anteriormente y mostrar cómo se comportan en torno a los procesos que atrapan salidas. Primero, establezcamos las bases para experimentar sin un proceso del sistema. Mostraré sucesivamente los resultados de lanzamientos, errores y salidas no atrapados en procesos vecinos:

Exception source: spawn_link(fun() -> ok end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.61.0>, normal}
The process exited normally, without a problem. Note that this looks a bit like the result of catch exit(normal), except a PID is added to the tuple to know what processed failed.
Exception source: spawn_link(fun() -> exit(reason) end)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.55.0>, reason}
The process has terminated for a custom reason. In this case, if there is no trapped exit, the process crashes. Otherwise, you get the above message.
Exception source: spawn_link(fun() -> exit(normal) end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.58.0>, normal}
This successfully emulates a process terminating normally. In some cases, you might want to kill a process as part of the normal flow of a program, without anything exceptional going on. This is the way to do it.
Exception source: spawn_link(fun() -> 1/0 end)
Untrapped ResultError in process <0.44.0> with exit value: {badarith, [{erlang, '/', [1,0]}]}
Trapped Result{'EXIT', <0.52.0>, {badarith, [{erlang, '/', [1,0]}]}}
The error ({badarith, Reason}) is never caught by a try ... catch block and bubbles up into an 'EXIT'. At this point, it behaves exactly the same as exit(reason) did, but with a stack trace giving more details about what happened.
Exception source: spawn_link(fun() -> erlang:error(reason) end)
Untrapped ResultError in process <0.47.0> with exit value: {reason, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.74.0>, {reason, [{erlang, apply, 2}]}}
Pretty much the same as with 1/0. That's normal, erlang:error/1 is meant to allow you to do just that.
Exception source: spawn_link(fun() -> throw(rocks) end)
Untrapped ResultError in process <0.51.0> with exit value: {{nocatch, rocks}, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.79.0>, {{nocatch, rocks}, [{erlang, apply, 2}]}}
Because the throw is never caught by a try ... catch, it bubbles up into an error, which in turn bubbles up into an EXIT. Without trapping exit, the process fails. Otherwise it deals with it fine.


Y eso es todo en lo que respecta a las excepciones habituales. Las cosas son normales: todo va bien. Suceden cosas excepcionales: los procesos mueren, se envían diferentes señales.

Luego está exit/2. Este es el equivalente a un arma en el proceso Erlang. Permite que un proceso mate a otro a distancia, de forma segura. Estas son algunas de las posibles llamadas:


Exception source: exit(self(), normal)
Untrapped Result** exception exit: normal
Trapped Result{'EXIT', <0.31.0>, normal}
When not trapping exits, exit(self(), normal) acts the same as exit(normal). Otherwise, you receive a message with the same format you would have had by listening to links from foreign processes dying.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), normal)
Untrapped Result: - nothing -
Trapped Result: - nothing -
This basically is a call to exit(Pid, normal). This command doesn't do anything useful, because a process can not be remotely killed with the reason normal as an argument.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), reason)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.52.0>, reason}
This is the foreign process terminating for reason itself. Looks the same as if the foreign process called exit(reason) on itself.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), kill)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.58.0>, killed}
Surprisingly, the message gets changed from the dying process to the spawner. The spawner now receives killed instead of kill. That's because kill is a special exit signal. More details on this later.
Exception source: exit(self(), kill)
Untrapped Result** exception exit: killed
Trapped Result** exception exit: killed
Oops, look at that. It seems like this one is actually impossible to trap. Let's check something.
Exception source: spawn_link(fun() -> exit(kill) end)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.67.0>, kill}
Now that's getting confusing. When another process kills itself with exit(kill) and we don't trap exits, our own process dies with the reason killed. However, when we trap exits, things don't happen that way.


Aunque se pueden atrapar la mayoría de las razones de salida, hay situaciones en las que se puede querer asesinar brutalmente un proceso: tal vez uno de ellos esté atrapando salidas pero también está atascado en un bucle infinito, sin leer ningún mensaje. La razón de eliminación actúa como una señal especial que no se puede atrapar. Esto garantiza que cualquier proceso que se termine con ella realmente estará muerto. Por lo general, matar es un último recurso, cuando todo lo demás ha fallado.

Como la razón de eliminación nunca se puede atrapar, se debe cambiar a matar cuando otros procesos reciben el mensaje. Si no se cambiara de esa manera, todos los demás procesos vinculados a ella morirían a su vez por la misma razón de eliminación y, a su vez, matarían a sus vecinos, y así sucesivamente. Se produciría una cascada de muertes.

Esto también explica por qué exit(kill) parece muerto cuando se recibe de otro proceso vinculado (la señal se modifica para que no se produzca una cascada), pero sigue pareciendo muerto cuando se atrapa localmente.

Si todo esto le parece confuso, no se preocupe. Muchos programadores sienten lo mismo. Las señales de salida son un poco curiosas. Afortunadamente, no hay muchos más casos especiales que los descritos anteriormente. Una vez que los comprenda, podrá comprender la mayor parte de la gestión de errores concurrentes de Erlang sin problemas.