Superviseurs à Elixir

Dans mon article précédent, nous parlions de Open Telecom Platform (OTP) et, plus spécifiquement, l'abstraction GenServer qui simplifie le travail avec les processus du serveur. Comme vous vous en souvenez probablement, GenServer est un comportement-pour l'utiliser, vous devez définir un module de rappel spécial qui satisfait au contrat dicté par ce comportement..

Ce que nous n'avons pas discuté, cependant, c'est la gestion des erreurs. Je veux dire, n'importe quel système peut éventuellement rencontrer des erreurs, et il est important de les éliminer correctement. Vous pouvez vous référer à l'article Comment gérer les exceptions dans Elixir pour en savoir plus sur les essayer / sauvetage bloc, élever, et quelques autres solutions génériques. Ces solutions sont très similaires à celles que l'on trouve dans d'autres langages de programmation courants, tels que JavaScript ou Ruby.. 

Pourtant, il y a plus à ce sujet. Après tout, Elixir est conçu pour construire des systèmes concurrents et tolérants aux pannes. Il a donc d'autres avantages à offrir. Dans cet article, nous parlerons des superviseurs, qui nous permettent de surveiller les processus et de les redémarrer après leur arrêt. Les superviseurs ne sont pas si complexes, mais assez puissants. Ils peuvent être facilement modifiés, mis en place avec diverses stratégies sur la façon de procéder aux redémarrages, et utilisés dans les arborescences de supervision.

Donc, aujourd'hui, nous verrons les superviseurs en action!

Les préparatifs

À des fins de démonstration, nous allons utiliser un exemple de code de mon article précédent sur GenServer. Ce module s'appelle CalcServer, et cela nous permet d'effectuer divers calculs et de conserver le résultat.

D'abord, créez un nouveau projet en utilisant le mix new calc_server commander. Ensuite, définissez le module, incluez GenServer, et fournir le début / 1 raccourci:

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

Ensuite, fournissez le init / 1 rappel qui sera exécuté dès le démarrage du serveur. Il prend une valeur initiale et utilise une clause de garde pour vérifier si c'est un nombre. Sinon, le serveur se termine:

def init (initial_value) lorsque is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "La valeur doit être un entier!" end

Désormais, les fonctions d’interface de code permettent d’ajouter, de diviser, de multiplier, de calculer la racine carrée et d’extraire le résultat (bien sûr, vous pouvez ajouter davantage d’opérations mathématiques à votre guise):

 def sqrt do GenServer.cast (__ MODULE__,: sqrt) fin def add (numéro) GenServer.cast (__ MODULE__, : add, number) end def multiply (nombre) do GenServer.cast (__ MODULE__, : multiply, number ) end def div (nombre) do GenServer.cast (__ MODULE__, : div, nombre) end def résultat do GenServer.call (__ MODULE__,: result) end

La plupart de ces fonctions sont gérées asynchrone, ce qui signifie que nous n'attendons pas qu'ils soient terminés. Cette dernière fonction est synchrone parce que nous voulons réellement attendre le résultat. Par conséquent, ajoutez handle_call et handle_cast callbacks:

 def handle_call (: résultat, _, état) do : réponse, état, état fin def handle_cast (opération, état) cas opération à faire do: sqrt -> : noreply,: math.sqrt (état) : multiplier , multiplicateur -> : noreply, état * multiplicateur : div, nombre -> : noreply, état / nombre : add, nombre -> : noreply, état + nombre _ -> : stop, "Non implémenté", state end end

En outre, spécifiez quoi faire si le serveur est arrêté (nous jouons ici à Captainident):

 def terminate (_reason, _state) do IO.puts "Le serveur s'est arrêté" fin

Le programme peut maintenant être compilé en utilisant iex -S mix et utilisé de la manière suivante:

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

Le problème est que le serveur se bloque lorsqu'une erreur est générée. Par exemple, essayez de diviser par zéro:

CalcServer.start (6.1) CalcServer.div (0) # [erreur] GenServer CalcServer terminant # ** (ArithmeticError) argument incorrect dans l'expression arithmétique # (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_app_ / 3 # dernier message: : "$ gen_cast", : div, 0 # State: 6.1 CalcServer.result |> IO.puts # ** (exit) est entré dans: GenServer.call (CalcServer,: result, 5000) # ** (EXIT ) no process: le processus n'est pas actif ou aucun processus n'est actuellement associé au nom indiqué, probablement parce que l'application n'est pas lancée # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Le processus est donc terminé et ne peut plus être utilisé. C'est vraiment mauvais, mais nous allons régler ce problème très bientôt!

Let It Crash

Chaque langage de programmation a ses idiomes, tout comme Elixir. Lorsque vous traitez avec des superviseurs, une approche courante consiste à laisser un processus en panne et à faire quelque chose à ce sujet - probablement, redémarrez et continuez.. 

Beaucoup de langages de programmation utilisent seulement essayer et capture (ou constructions similaires), qui est un style de programmation plus défensif. Nous essayons fondamentalement d'anticiper tous les problèmes possibles et de fournir un moyen de les résoudre. 

Les choses sont très différentes chez les superviseurs: si un processus se bloque, il se bloque. Mais le superviseur, tout comme un brave médecin de combat, est là pour aider un processus déchu à récupérer. Cela peut sembler un peu étrange, mais en réalité, c'est une logique très saine. De plus, vous pouvez même créer des arborescences de supervision et ainsi isoler les erreurs, empêchant ainsi l'application dans son ensemble de tomber en panne si l'une de ses parties rencontrait des problèmes..

Imaginez conduire une voiture: elle est composée de différents sous-systèmes et vous ne pouvez pas les vérifier à chaque fois. Ce que vous pouvez faire, c'est réparer un sous-système s'il se casse (ou bien demander à un mécanicien de le faire) et continuer votre voyage. Les superviseurs d’Elixir font justement cela: ils surveillent vos processus (appelés processus enfants) et les redémarrer au besoin.

Créer un superviseur

Vous pouvez implémenter un superviseur à l'aide du module de comportement correspondant. Il fournit des fonctions génériques pour le traçage et le compte rendu des erreurs.

Tout d’abord, vous devez créer un lien à votre superviseur. La liaison est également une technique très importante: lorsque deux processus sont liés et que l'un d'eux se termine, un autre reçoit une notification avec un motif de sortie. Si le processus lié s'est arrêté de manière anormale (c'est-à-dire qu'il s'est bloqué), son homologue est également fermé..

Ceci peut être démontré en utilisant les fonctions spawn / 1 et spawn_link / 1:

spawn (fn -> IO.puts "salut du parent!" spawn_link (fn -> IO.puts "salut de l'enfant!" fin) fin)

Dans cet exemple, nous engendrons deux processus. La fonction interne est créée et liée au processus en cours. Maintenant, si vous signalez une erreur dans l’une d’elles, une autre se terminera également:

spawn (fn -> IO.puts "bonjour du parent!" spawn_link (fn -> IO.puts bonjour de l'enfant! "raise (" oops. ") end): timer.sleep (2000) IO.puts" inaccessible! "fin) # [erreur] Traiter #PID<0.83.0> a soulevé une exception # ** (RuntimeError) oops. # gen.ex: 5: anonyme fn / 0 dans: elixir_compiler_0 .__ FICHIER __ / 1

Donc, pour créer un lien lorsque vous utilisez GenServer, remplacez simplement votre début fonctions avec start_link:

defmodule CalcServer utilise GenServer def start_link (initial_value) do GenServer.start_link (__ MODULE__, initial_value, name: __MODULE__) end #… end

Tout est question de comportement

Maintenant, bien sûr, un superviseur devrait être créé. Ajouter un nouveau lib / calc_supervisor.ex fichier avec le contenu suivant:

defmodule CalcSupervisor utilise Supervisor def start_link do Supervisor.start_link (__ MODULE__, nil) end def (_) surveille ([worker (CalcServer, [0])], stratégie:: one_for_one) end end 

Il y a beaucoup de choses qui se passent ici, alors allons-y doucement.

start_link / 2 est une fonction permettant de démarrer le superviseur actuel. Notez que le processus enfant correspondant sera également lancé, vous n’aurez donc pas à taper CalcServer.start_link (5) plus.

init / 2 est un rappel qui doit être présent pour pouvoir utiliser le comportement. le superviser fonction, en gros, décrit ce superviseur. À l'intérieur, vous spécifiez les processus enfants à superviser. Nous spécifions bien sûr les CalcServer processus de travail. [0] ici signifie l'état initial du processus, c'est la même chose que de dire CalcServer.start_link (0).

:un pour un est le nom de la stratégie de redémarrage du processus (ressemblant à une devise célèbre de Musketeers). Cette stratégie dicte que lorsqu'un processus enfant se termine, un nouveau processus doit être démarré. Il existe une poignée d'autres stratégies disponibles:

  • :un pour tous (encore plus de style mousquetaire!) - redémarrer tous les processus si on se termine.
  • : rest_for_one-les processus enfants démarrés après le processus terminé sont redémarrés. Le processus terminé est également redémarré.
  • : simple_one_pour_one-similaire à one_for_one mais ne nécessite qu'un seul processus enfant dans la spécification. Utilisé lorsque le processus supervisé doit être démarré et arrêté dynamiquement.

Donc, l’idée générale est assez simple:

  • Tout d'abord, un processus de supervision est lancé. le init callback doit renvoyer une spécification expliquant les processus à surveiller et la gestion des plantages.
  • Les processus enfants supervisés sont démarrés conformément à la spécification.
  • Après le blocage d’un processus enfant, les informations sont envoyées au superviseur grâce au lien établi. Le superviseur suit alors la stratégie de redémarrage et effectue les actions nécessaires..

Maintenant, vous pouvez relancer votre programme et essayer de diviser par zéro:

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

Donc, l'état est perdu, mais le processus est en cours même si une erreur s'est produite, ce qui signifie que notre superviseur fonctionne bien!

Ce processus enfant est à toute épreuve, et vous aurez littéralement du mal à le tuer:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, je suis immortel!

Notez toutefois que techniquement, le processus n'est pas redémarré. Au lieu de cela, un nouveau processus est en cours de démarrage, de sorte que l'ID de processus ne sera pas le même. Cela signifie fondamentalement que vous devez donner des noms à vos processus lors de leur démarrage..

L'application

Vous trouverez peut-être un peu fastidieux de démarrer le superviseur manuellement à chaque fois. Heureusement, il est assez facile de corriger en utilisant le module Application. Dans le cas le plus simple, vous n'aurez besoin que de deux modifications.

Tout d’abord, modifiez le mix.exs fichier situé à la racine de votre projet:

 #… Def application do # Spécifiez les applications supplémentaires que vous utiliserez depuis Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

Ensuite, incluez le Application module et fournissez le rappel start / 2 qui sera exécuté automatiquement au démarrage de votre application:

defmodule CalcServer utilise Application utilise GenServer def start (_type, _args) do CalcSupervisor.start_link end #… end

Maintenant, après avoir exécuté le iex -S mix commande, votre superviseur sera immédiatement opérationnel!

Redémarrage infini?

Vous pouvez vous demander ce qui va se passer si le processus se bloque constamment et si le superviseur correspondant le redémarre à nouveau. Ce cycle fonctionnera-t-il indéfiniment? En fait, non. Par défaut, seulement 3 redémarre dans 5 les secondes sont autorisées, pas plus que cela. Si plusieurs redémarrages se produisent, le superviseur abandonne et se tue, ainsi que tous les processus enfants. Cela semble horrible, hein?

Vous pouvez facilement le vérifier en exécutant rapidement la ligne de code suivante maintes et maintes fois (ou en effectuant un cycle):

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

Vous pouvez modifier deux options pour modifier ce comportement:

  • : max_restarts-combien de redémarrages sont autorisés dans le délai imparti
  • : max_seconds-le délai réel

Ces deux options devraient être transmises au superviser fonctionner à l'intérieur du init rappeler:

 def init (_) supervise ([worker (CalcServer, [0])], max_restarts: 5, max_seconds: 6, stratégie:: one_for_one) fin

Conclusion

Dans cet article, nous avons parlé des superviseurs Elixir, qui nous permettent de surveiller et de redémarrer les processus enfants si nécessaire. Nous avons vu comment ils peuvent surveiller vos processus et les redémarrer au besoin, et comment ajuster divers paramètres, y compris les stratégies de redémarrage et les fréquences..

J'espère que vous avez trouvé cet article utile et intéressant. Je vous remercie de rester avec moi et jusqu'à la prochaine fois!