aprendiendo ( Erlang ).

martes, 18 de octubre de 2011

Control de errores en sistemas concurrentes (link y trap_exit)

| 0 comentarios |

Como ya hemos visto Erlang, como otros muchos lenguajes, implementa un sistema de manejo de excepciones para la captura de errores, en partes de nuestro código. Pero en lo que es único Erlang es en el linkado de procesos para el manejo de fallos de procesos. Suponte que un proceso depende del funcionamiento de otro. En este caso, nos puede ser muy útil poder vigilarlo para saber que esta operativo. Es para esto, para tener vigilado al proceso, para lo que sirve el linkado de procesos.
Cuando un proceso Erlang muere inesperadamente genera una señal de salida (exit). Pues bien, todos los procesos linkados al proceso moribundo recibirían la señal de salida y terminarán su ejecución. El comportamiento por defecto, es que el proceso que linka reciba la señal de salida (terminé) y también todos los procesos que estén linkados a él y así sucesivamente, hasta que todos lo procesos vinculados directa o indirectamente hayan recibido la señal y finalicen la ejecución. Este comportamiento en cascada nos permite disponer de grupos de procesos que se comportan como una única aplicación, evitando que tengamos que buscar procesos perdidos antes de poder reiniciarla.

Podemos ver en la ilustración anterior como el proceso A esta linkado al proceso B, y como cuando el proceso B termina o muere envía una señal de salida al proceso A que a su vez termina. Sin embargo, podemos variar este comportamiento según nuestras necesidades. Para ello, el proceso A debe atrapar la señal de salida y procesarla decidiendo que es lo que desea hacer en ese momento.

En la ilustración anterior podemos ver el comportamiento de un proceso A que esta linkado a B y además, atrapa la señal de salida. Al proceso que atrapa la salida se le llama System Process. En este caso, el proceso A podría decidir terminar con su ejecución o continuar.
Pero, ¿Cómo sabe el proceso B a quién tiene que avisar? El proceso B tiene lo que se llama un conjunto de procesos linkados. Es decir, cuando un proceso, en nuestro caso A, se linka a otro proceso este, en nuestro caso B, registra su PID en un conjunto de procesos linkados a él. De esta forma cuando el proceso linkado termina, B, recorre el conjunto de procesos linkados enviando la señal de salida.
Queda por decir, que existe una señal de salida especial. Si una señal de salida tiene como motivo kill, entonces el proceso linkado, sea o no un System Process, morirá. Se trata de una señal intratable.
Ahora, vamos a resumir los distintos comportamientos en la siguiente tabla:
Atrapar salida Señal de salida Comportamiento
Si kill Proceso muere y broadcast de la señal kill al conjunto de procesos linkados.
Si Motivo Se recibe un mensaje {’EXIT’, Pid, Motivo}.
No normal Continua no hace nada. Ignora la señal.
No kill Proceso muere y broadcast de la señal kill al conjunto de procesos linkados.
No Motivo Muere y broadcast de la señal Motivo al conjunto de procesos linkados.


Funciones

Todo lo que teóricamente nos ha constado tanto explicar, a la hora de la práctica, es muy fácil de implementar. Para empezar, necesitamos de muy pocas funciones:
  • link(Pid): como bien indica linka nuestro proceso a otro.
  • spawn_link: crea un nuevo proceso y lo linka inmediatamente.
  • process_flag(trap_exit, Bool): si ponemos true indicamos que deseamos atrapar las señales de salida y convertimos nuestro proceso en un System Process. Por defecto, es false.


Templates

Existen tres fórmulas magistrales para el control del errores en sistemas concurrentes. Con estas fórmulas se contemplan casi el 100% de los posibilidades que se dan en la mayoría de los sistemas.
Las fórmulas magistrales son:
  1. No me importa que termine un proceso: se crea un proceso con spawn. Así si el proceso creado falla el proceso actual continua.
    Pid = spawn ( fun() -> ... end )
      
  2. Quiero morir si mi proceso hijo muere: siempre que el proceso creado no termine con una señal de salida normal el proceso que lo crea morirá.
    Pid = spawn_link ( fun() -> ... end )
      
  3. Quiero manejar los errores si el proceso que creo falla: en este caso, no sólo linkamos el proceso que creamos sino que atrapamos las señales de salida.
    ...
        process_flag(trap_exit, true),
        Pid = spawn_link(fun() -> ... end),
        ...
        loop(...).
        
        loop(State) ->
           receive
              {'EXIT', Pid, Motivo} ->  %% tratamos el error capturado
                 loop(State1);
              ...
           end
    
      


El ejemplo

Vamos a crear un pequeño módulo de ejemplo que nos sirva para ilustra como los procesos atrapan las señales de salida.
-module(prueba).

-export([start/3]).

start(Atrapa_A, Atrapa_B, Mensaje) ->
    Pid_A = spawn(fun() -> proceso_a(Atrapa_A) end),
    Pid_B = spawn(fun() -> proceso_b(Pid_A, Atrapa_B) end),
    Pid_C = spawn(fun() -> proceso_c(Pid_B, Mensaje) end),
    sleep(1000),
    estado(procesoA, Pid_A),
    estado(procesoB, Pid_B),
    estado(procesoC, Pid_C).

proceso_a(Atrapar) ->
    process_flag(trap_exit, Atrapar),
    wait(procesoA).

proceso_b(Pid, Atrapar) ->
    process_flag(trap_exit, Atrapar),
    link(Pid),
    wait(procesoB).

proceso_c(Pid, Mensaje) ->
    link(Pid),
    case Mensaje of
        {matar, Motivo} ->
            exit(Motivo);
        {dividir, N} ->
            1/N,
            wait(procesoC);
        normal ->
            true
    end.

wait (Proceso) ->
    receive
        Recibido ->
            io:format("Proceso ~p recibido ~p~n" ,[Proceso, Recibido]),
            wait(Proceso)
    end.

estado (Nombre, Pid) ->
    case erlang:is_process_alive(Pid) of
        true ->
            io:format("proceso ~p (~p) esta vivo~n" , [Nombre, Pid]);
        false ->
            io:format("proceso ~p (~p) esta muerto~n" , [Nombre, Pid])
    end.

sleep(Milisegundos) ->
    receive
    after Milisegundos ->
            true
    end.
La función start/3 se encarga de crear tres procesos a, b y c. Los procesos a y b pueden ser System Process, es decir, que pueden atrapar las señales de salida. El proceso c es un proceso normal que sólo nos proporciona errores.
La función wait/1 se encarga de esperar y procesar los mensajes recibidos para todos los procesos.
Quizás la función que te puede llamar la atención es estado/2. Dicha función hace uso de una función del sistema que indica si un proceso esta vivo o muerto, esta función es erlang:is_process_alive/1.
Veamos el comportamiento de nuestro procesos en los siguientes casos:
  1. Los procesos a y b son System Process:
    1> prueba:start(true, true, {matar, motivo}).
    Proceso procesoB recibido {'EXIT',<0.47.0>,motivo}
    proceso procesoA (<0.45.0>) esta vivo
    proceso procesoB (<0.46.0>) esta vivo
    proceso procesoC (<0.47.0>) esta muerto
    ok
    2> prueba:start(true, true, {divide, 0}).    
    
    =ERROR REPORT==== 16-Oct-2011::23:50:42 ===
    Error in process <0.51.0> with exit value: {{case_clause,{divide,0}},[{prueba,proceso_c,2}]}
    
    Proceso procesoB recibido {'EXIT',<0.51.0>,
                                      {{case_clause,{divide,0}},
                                       [{prueba,proceso_c,2}]}}
    proceso procesoA (<0.49.0>) esta vivo
    proceso procesoB (<0.50.0>) esta vivo
    proceso procesoC (<0.51.0>) esta muerto   
      
    Podemos observar como los procesos a y b sobreviven a los errores provocados por el proceso c.
  2. Sólo el proceso a es un System Process:
    3> prueba:start(true, false, {matar, motivo}).
    Proceso procesoA recibido {'EXIT',<0.62.0>,motivo}
    proceso procesoA (<0.61.0>) esta vivo
    proceso procesoB (<0.62.0>) esta muerto
    proceso procesoC (<0.63.0>) esta muerto
    ok
    4> prueba:start(true, false, {divide, 0}).    
    Proceso procesoA recibido {'EXIT',<0.66.0>,
                                      {{case_clause,{divide,0}},
                                       [{prueba,proceso_c,2}]}}
    
    =ERROR REPORT==== 17-Oct-2011::00:07:17 ===
    Error in process <0.67.0> with exit value: {{case_clause,{divide,0}},[{prueba,proceso_c,2}]}
    
    proceso procesoA (<0.65.0>) esta vivo
    proceso procesoB (<0.66.0>) esta muerto
    proceso procesoC (<0.67.0>) esta muerto
    ok
      
    Sólo el proceso a sobrevive.
  3. No hay System Process:
    5> prueba:start(false, false, {matar, motivo}).
    proceso procesoA (<0.69.0>) esta muerto
    proceso procesoB (<0.70.0>) esta muerto
    proceso procesoC (<0.71.0>) esta muerto
    ok
    6> prueba:start(false, false, {divide, 0}).    
    
    =ERROR REPORT==== 17-Oct-2011::00:13:40 ===
    Error in process <0.75.0> with exit value: {{case_clause,{divide,0}},[{prueba,proceso_c,2}]}
    
    proceso procesoA (<0.73.0>) esta muerto
    proceso procesoB (<0.74.0>) esta muerto
    proceso procesoC (<0.75.0>) esta muerto
    ok
      


Tolerancia a fallos

Joe Armstrong nos dice, con muy buen criterio, que para tener un sistema tolerante a fallos necesitamos dos ordenadores. Uno que realice el trabajo y otro que supervise al primer ordenador. De esta forma podemos saber que hacer en el caso de fallos eventuales como parar el sistema o recuperarlo.
En Erlang los patrones para sistemas tolerantes a fallos, son llamados modelos worker-supervisor. Existen en OTP librerías que definen el comportamiento de un supervisor.
Sabiendo como se comportan los principios de procesos enlazados y los System Process hemos dado un paso importante para entender el modelo worker-supervisor y para crear sistemas más tolerantes a fallos.

Publicar un comentario

0 comentarios:

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.