Supervisores en Elixir

En mi artículo anterior estábamos hablando Plataforma abierta de telecomunicaciones (OTP) y, más específicamente, la abstracción GenServer que facilita el trabajo con los procesos del servidor. GenServer, como probablemente recuerdes, es un comportamiento-para usarlo, debe definir un módulo especial de devolución de llamada que satisfaga el contrato según lo dictado por este comportamiento.

Lo que no hemos discutido, sin embargo, es manejo de errores. Quiero decir, cualquier sistema puede eventualmente experimentar errores, y es importante tomarlos adecuadamente. Puede consultar el artículo Cómo manejar las excepciones en Elixir para obtener más información acerca de intentar / rescatar bloquear, aumento, y algunas otras soluciones genéricas. Estas soluciones son muy similares a las que se encuentran en otros lenguajes de programación populares, como JavaScript o Ruby. 

Aún así, hay más de este tema. Después de todo, Elixir está diseñado para construir sistemas concurrentes y tolerantes a fallas, por lo que tiene otras ventajas que ofrecer. En este artículo hablaremos sobre los supervisores, que nos permiten monitorear los procesos y reiniciarlos después de que terminen. Los supervisores no son tan complejos, sino muy poderosos. Se pueden modificar fácilmente, configurar con varias estrategias sobre cómo realizar reinicios y utilizarlos en árboles de supervisión..

Así que hoy veremos a los supervisores en acción.!

Preparativos

Para fines de demostración, vamos a utilizar algunos ejemplos de código de mi artículo anterior sobre GenServer. Este modulo se llama CalcServer, y nos permite realizar varios cálculos y persistir el resultado..

De acuerdo, primero, cree un nuevo proyecto usando el mezclar nuevo calc_server mando. A continuación, defina el módulo, incluya GenServer, y proporcionar el inicio / 1 atajo:

# lib / calc_server.ex defmodule CalcServer no usa GenServer def start (initial_value) do GenServer.start (__ MODULE__, initial_value, name: __MODULE__) end end end

A continuación, proporcione la init / 1 devolución de llamada que se ejecutará tan pronto como se inicie el servidor. Toma un valor inicial y usa una cláusula de protección para verificar si es un número. Si no, el servidor termina:

def init (initial_value) cuando is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "¡El valor debe ser un número entero!" end

Ahora las funciones de la interfaz de código para realizar la suma, la división, la multiplicación, el cálculo de la raíz cuadrada y la obtención del resultado (por supuesto, puede agregar más operaciones matemáticas según sea necesario):

 def sqrt do GenServer.cast (__ MODULE__,: sqrt) end def add (número) do GenServer.cast (__ MODULE__, : add, number) end def multiply (número) do GenServer.cast (__ MODULE__, : multiply, number ) end def div (número) do GenServer.cast (__ MODULE__, : div, number) end def result do do GenServer.call (__ MODULE__,: result) end

La mayoría de estas funciones son manejadas. asíncrono, lo que significa que no estamos esperando a que se completen. La última función es sincrónico Porque en realidad queremos esperar a que llegue el resultado. Por lo tanto, añadir handle_call y handle_cast devoluciones de llamada:

 def handle_call (: result, _, state) do : reply, state, state end def handle_cast (operation, state) do case operation do: sqrt -> : noreply,: math.sqrt (state) : multiply , multiplicador -> : noreply, estado * multiplicador : div, número -> : noreply, estado / número : sumar, número -> : noreply, estado + número _ -> : detener, "No implementado", estado fin fin

Además, especifique qué hacer si el servidor se termina (estamos jugando a Captain Obvious aquí):

 def terminate (_reason, _state) do IO.puts "The server terminated" final

El programa ahora se puede compilar usando mezcla iex -S y utilizado de la siguiente manera:

CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875

El problema es que el servidor se bloquea cuando se produce un error. Por ejemplo, trate de dividir por cero:

CalcServer.start (6.1) CalcServer.div (0) # [error] GenServer CalcServer que termina # ** (ArithmeticError) argumento incorrecto en la expresión aritmética # (calc_server) lib / calc_server.ex: 44: CalcServer.handle_cast / 2 # (stdlib ) gen_server.erl: 601: gen_server.try_dispatch / 4 # (stdlib) gen_server.erl: 667:: gen_server.handle_msg / 5 # (stdlib) proc_lib.erl: 247: proc_lib.init_p_do_apply / # Último mensaje: : "$ gen_cast", : div, 0 # Estado: 6.1 CalcServer.result |> IO.puts # ** (exit) salió en: GenServer.call (CalcServer,: result, 5000) # ** (EXIT ) ningún proceso: el proceso no está vivo o no hay ningún proceso asociado actualmente con el nombre dado, posiblemente porque su aplicación no se inició # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Por lo tanto, el proceso se termina y ya no se puede utilizar. Esto es realmente malo, pero vamos a solucionar esto muy pronto!

Let It Crash

Cada lenguaje de programación tiene sus modismos, y también el elixir. Cuando se trata con supervisores, un enfoque común es dejar que un proceso se bloquee y luego hacer algo al respecto, probablemente, reiniciar y continuar. 

Muchos lenguajes de programación usan solo tratar y captura (o construcciones similares), que es un estilo de programación más defensivo. Básicamente estamos tratando de anticipar todos los problemas posibles y proporcionar una manera de superarlos.. 

Las cosas son muy diferentes con los supervisores: si un proceso se bloquea, se bloquea. Pero el supervisor, al igual que un valiente médico de batalla, está ahí para ayudar a un proceso caído a recuperarse. Esto puede sonar un poco extraño, pero en realidad es una lógica muy sensata. Además, incluso puede crear árboles de supervisión y de esta manera aislar los errores, evitando que la aplicación se bloquee si una de sus partes tiene problemas..

Imagínese conduciendo un automóvil: está compuesto por varios subsistemas, y no puede verificarlos cada vez. Lo que puede hacer es arreglar un subsistema si se rompe (o, bueno, pedirle a un mecánico de automóviles que lo haga) y continuar su viaje. Los supervisores en Elixir hacen precisamente eso: supervisan sus procesos (denominados procesos infantiles) y reinicie según sea necesario.

Creación de un supervisor

Puede implementar un supervisor utilizando el módulo de comportamiento correspondiente. Proporciona funciones genéricas para el seguimiento de errores e informes..

En primer lugar, necesitarías crear un enlazar a su supervisor La vinculación también es una técnica bastante importante: cuando dos procesos están vinculados entre sí y uno de ellos finaliza, otro recibe una notificación con un motivo de salida. Si el proceso vinculado finalizó de forma anormal (es decir, se bloqueó), su contraparte también se cierra.

Esto se puede demostrar usando las funciones spawn / 1 y spawn_link / 1:

spawn (fn -> IO.puts "hi from parent!" spawn_link (fn -> IO.puts "hi from child!" end))

En este ejemplo, estamos generando dos procesos. La función interna se genera y se vincula al proceso actual. Ahora, si genera un error en uno de ellos, otro terminará también:

spawn (fn -> IO.puts "hi from parent!" spawn_link (fn -> IO.puts "hi from child!" raise ("oops.") end): timer.sleep (2000) IO.puts "inalcanzable! "fin) # [error] Proceso #PID<0.83.0> levantó una excepción # ** (RuntimeError) oops. # gen.ex: 5: anónimo fn / 0 en: elixir_compiler_0 .__ FILE __ / 1

Entonces, para crear un enlace cuando use GenServer, simplemente reemplace su comienzo funciona con start_link:

defmódulo CalcServer no usa GenServer def start_link (initial_value) do GenServer.start_link (__ MODULE__, initial_value, name: __MODULE__) end #… end

Es todo sobre el comportamiento

Ahora, por supuesto, se debe crear un supervisor. Añadir un nuevo lib / calc_supervisor.ex Archivo con los siguientes contenidos:

defmódulo CalcSupervisor utiliza Supervisor def start_link do Supervisor.start_link (__ MODULE__, nil) end def init (_) supervise ([worker (CalcServer, [0])], estrategia:: one_for_one) end end end 

Hay mucho que hacer aquí, así que avancemos lentamente..

start_link / 2 es una función para iniciar el supervisor real. Tenga en cuenta que el proceso hijo correspondiente también se iniciará, por lo que no tendrá que escribir CalcServer.start_link (5) nunca más.

init / 2 es una devolución de llamada que debe estar presente para poder emplear el comportamiento. los supervisar La función, básicamente, describe a este supervisor. En el interior se especifica qué procesos secundarios supervisar. Estamos, por supuesto, especificando el CalcServer proceso de trabajo. [0] aquí significa el estado inicial del proceso, es lo mismo que decir CalcServer.start_link (0).

:uno por uno es el nombre de la estrategia de reinicio del proceso (que se asemeja a un famoso lema de los Mosqueteros). Esta estrategia determina que cuando finaliza un proceso secundario, se debe iniciar uno nuevo. Hay un puñado de otras estrategias disponibles:

  • :uno para todos (¡incluso más estilo Mosquetero!) - reinicie todos los procesos si uno termina.
  • : rest_for_one-Los procesos secundarios se inician después de que se reinicie el finalizado. El proceso terminado también se reinicia.
  • : simple_one_for_one-similar a: one_for_one pero requiere que solo un proceso hijo esté presente en la especificación. Se utiliza cuando el proceso supervisado debe iniciarse y detenerse dinámicamente..

Así que la idea general es bastante simple:

  • En primer lugar, se inicia un proceso de supervisión. los en eso La devolución de llamada debe devolver una especificación que explique qué procesos monitorear y cómo manejar los bloqueos..
  • Los procesos secundarios supervisados ​​se inician de acuerdo con la especificación..
  • Después de que un proceso hijo se bloquea, la información se envía al supervisor gracias al enlace establecido. El supervisor sigue la estrategia de reinicio y realiza las acciones necesarias..

Ahora puedes ejecutar tu programa de nuevo y tratar de dividir por cero:

CalcSupervisor.start_link CalcServer.add (10) CalcServer.result # => 10 CalcServer.div (0) # => error! CalcServer.result # => 0

Por lo tanto, se pierde el estado, pero el proceso se está ejecutando aunque haya ocurrido un error, lo que significa que nuestro supervisor está trabajando bien.!

Este proceso secundario es bastante a prueba de balas, y literalmente te será difícil matarlo:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, soy inmortal!

Sin embargo, tenga en cuenta que, técnicamente, el proceso no se reinicia, sino que se está iniciando uno nuevo, por lo que la identificación del proceso no será la misma. Básicamente significa que debes dar nombres a tus procesos al iniciarlos..

La aplicación

Puede resultarle un poco tedioso iniciar el supervisor manualmente cada vez. Afortunadamente, es bastante fácil de solucionar mediante el uso del módulo de aplicación. En el caso más simple, solo necesitarás hacer dos cambios.

En primer lugar, ajustar la mix.exs Archivo ubicado en la raíz de su proyecto:

 #… Def application do # Especifique aplicaciones adicionales que usará de Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

A continuación, incluya el Solicitud módulo y proporcione el inicio / 2 de devolución de llamada que se ejecutará automáticamente cuando se inicie su aplicación:

defmodule CalcServer no usa la aplicación GenServer def start (_type, _args) do CalcSupervisor.start_link end #… end

Ahora después de ejecutar el mezcla iex -S comando, su supervisor estará listo y funcionando de inmediato!

Reinicios infinitos?

Puede preguntarse qué ocurrirá si el proceso se bloquea constantemente y el supervisor correspondiente lo reinicia nuevamente. ¿Este ciclo se ejecutará indefinidamente? Bueno, en realidad, no. Por defecto, solo 3 reinicia dentro 5 Se permiten segundos, no más que eso. Si ocurren más reinicios, el supervisor se rinde y se mata a sí mismo y a todos los procesos secundarios. Suena horrible, eh?

Puede comprobarlo fácilmente ejecutando rápidamente la siguiente línea de código una y otra vez (o haciéndolo en un ciclo):

Process.whereis (CalcServer) |> Process.exit (: kill) #… # ** (EXIT from #PID<0.117.0>) apagar 

Hay dos opciones que puedes modificar para cambiar este comportamiento:

  • : max_restarts-¿Cuántos reinicios están permitidos dentro del plazo?
  • : max_seconds-el período de tiempo real

Ambas opciones deben pasarse a la supervisar función dentro de la en eso llamar de vuelta:

 def init (_) sí supervisa ([worker (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategy:: one_for_one) end

Conclusión

En este artículo, hemos hablado sobre los Supervisores de Elixir, que nos permiten monitorear y reiniciar los procesos secundarios según sea necesario. Hemos visto cómo pueden monitorear sus procesos y reiniciarlos según sea necesario, y cómo ajustar varias configuraciones, incluyendo estrategias y frecuencias de reinicio..

Con suerte, encontraste este artículo útil e interesante. Te agradezco que te quedes conmigo y hasta la próxima.!