Betreuer in Elixir

In meinem vorherigen Artikel haben wir darüber gesprochen Open Telecom Platform (OTP) und insbesondere die GenServer-Abstraktion, die die Arbeit mit Serverprozessen vereinfacht. GenServer ist, wie Sie sich vielleicht erinnern, ein Verhalten-Um es zu verwenden, müssen Sie ein spezielles Rückrufmodul definieren, das den durch dieses Verhalten vorgegebenen Vertrag erfüllt.

Was wir jedoch nicht besprochen haben, ist Fehlerbehandlung. Ich meine, jedes System kann irgendwann auf Fehler stoßen, und es ist wichtig, sie richtig zu nehmen. Sie können den Artikel Ausnahmebedingungen im Elixir-Artikel lesen, um mehr über die versuchen / retten Block, erziehen, und einige andere generische Lösungen. Diese Lösungen sind denen in anderen gängigen Programmiersprachen wie JavaScript oder Ruby sehr ähnlich. 

Es gibt jedoch noch mehr zu diesem Thema. Schließlich ist Elixir so konzipiert, dass es parallele und fehlertolerante Systeme erstellt, so dass es weitere Vorteile bietet. In diesem Artikel werden wir über Vorgesetzte sprechen, die es uns ermöglichen, Prozesse zu überwachen und nach deren Beendigung erneut zu starten. Supervisoren sind nicht so komplex, aber ziemlich mächtig. Sie können leicht angepasst, mit verschiedenen Strategien für die Durchführung von Neustarts eingerichtet und in Überwachungsbäumen verwendet werden.

Heute werden wir Supervisoren in Aktion sehen!

Vorbereitungen

Zu Demonstrationszwecken verwenden wir einen Beispielcode aus meinem vorherigen Artikel über GenServer. Dieses Modul wird aufgerufen CalcServer, und es erlaubt uns, verschiedene Berechnungen durchzuführen und das Ergebnis zu behalten.

Also gut, erstens erstellen Sie ein neues Projekt mit der mix neuen calc_server Befehl. Definieren Sie als Nächstes das Modul include GenServer, und bieten die Start / 1 Abkürzung:

# lib / calc_server.ex defmodule CalcServer verwendet GenServer def start (Anfangswert), um GenServer.start (__ MODULE__, Anfangswert, Name: __MODULE__) zu beenden

Als nächstes geben Sie die init / 1 Rückruf, der ausgeführt wird, sobald der Server gestartet wird. Es nimmt einen Anfangswert und verwendet eine Guard-Klausel, um zu überprüfen, ob es sich um eine Zahl handelt. Wenn nicht, wird der Server beendet:

def init (initial_value) wenn is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "Der Wert muss eine ganze Zahl sein!" end

Code-Interface-Funktionen zum Hinzufügen, Dividieren, Multiplizieren, Berechnen der Quadratwurzel und Abrufen des Ergebnisses (natürlich können Sie nach Bedarf weitere mathematische Operationen hinzufügen):

 def sqrt do GenServer.cast (__ MODULE__,: sqrt) end def add (Nummer) do GenServer.cast (__ MODULE__, : add, number) Ende def multiplizieren (number) do GenServer.cast (__ MODULE__, : multiply, number) ) end def div (number) do GenServer.cast (__ MODULE__, : div, number) Ende def Ergebnis do GenServer.call (__ MODULE__,: result) ende

Die meisten dieser Funktionen werden behandelt asynchron, Das heißt, wir warten nicht auf ihre Vollendung. Die letzte Funktion ist synchron weil wir eigentlich auf das Ergebnis warten wollen. Deshalb addiere handle_call und handle_cast Rückrufe:

 def handle_call (: result, _, state) do : antworten, state, state end def handle_cast (operation, state) do case operation do: sqrt -> : noreply,: math.sqrt (state) : multiplizieren , Multiplikator -> : noreply, state * Multiplikator : div, number -> : noreply, state / number : addieren, number -> : noreply, state + number _ -> : Stop, "Nicht implementiert", Zustand Ende Ende

Geben Sie außerdem an, was zu tun ist, wenn der Server beendet wird (wir spielen hier Captain Obvious):

 def terminate (_reason, _state) do IO.puts "Der Server wurde beendet" beendet

Das Programm kann jetzt mit kompiliert werden iex -S mix und auf folgende Weise verwendet:

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

Das Problem ist, dass der Server abstürzt, wenn ein Fehler auftritt. Versuchen Sie beispielsweise, durch Null zu teilen:

CalcServer.start (6.1) CalcServer.div (0) # [error] GenServer CalcServer beendet # ** (ArithmeticError) ungültiges Argument im arithmetischen Ausdruck # (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 / 3 # Letzte Nachricht:  : "$ gen_cast", : div, 0 # State: 6.1 CalcServer.result |> IO.puts # ** (exit) wurde beendet in: GenServer.call (CalcServer,: result, 5000) # ** (EXIT.) ) Kein Prozess: Der Prozess ist nicht aktiv oder es ist derzeit kein Prozess mit dem angegebenen Namen verknüpft, möglicherweise weil seine Anwendung nicht gestartet ist # (Elixier) lib / gen_server.ex: 729: GenServer.call/3

Der Prozess ist also beendet und kann nicht mehr verwendet werden. Das ist in der Tat schlecht, aber wir werden das bald beheben!

Lass es abstürzen

Jede Programmiersprache hat ihre Redewendungen und Elixir auch. Im Umgang mit Supervisoren besteht ein gängiger Ansatz darin, einen Prozess zum Absturz bringen zu lassen und dann etwas dagegen zu unternehmen - wahrscheinlich neu zu starten und weiterzumachen. 

Viele Programmiersprachen werden nur verwendet Versuchen und Fang (oder ähnliche Konstrukte), was einen eher defensiven Programmierstil darstellt. Grundsätzlich versuchen wir, alle möglichen Probleme zu antizipieren und einen Weg zu finden, um sie zu lösen. 

Bei Supervisoren sieht es ganz anders aus: Wenn ein Prozess abstürzt, stürzt er ab. Aber der Supervisor ist genau wie ein tapferer Kampfmediziner da, um einem heruntergefallenen Prozess zu helfen. Das hört sich vielleicht etwas seltsam an, aber in Wirklichkeit ist das eine sehr logische Logik. Darüber hinaus können Sie sogar Überwachungsbäume erstellen und auf diese Weise Fehler isolieren, um zu verhindern, dass die gesamte Anwendung abstürzt, wenn bei einem ihrer Teile Probleme auftreten.

Stellen Sie sich vor, Sie fahren ein Auto: Es besteht aus verschiedenen Subsystemen, die Sie nicht jedes Mal überprüfen können. Sie können ein Subsystem reparieren, wenn es kaputt geht (oder einen Automechaniker dazu auffordert) und Ihre Reise fortsetzen. Supervisoren in Elixir tun genau das: Sie überwachen Ihre Prozesse (genannt Kindprozesse) und starten Sie sie bei Bedarf neu.

Supervisor erstellen

Sie können einen Supervisor mit dem entsprechenden Verhaltensmodul implementieren. Es bietet allgemeine Funktionen zur Fehlersuche und Berichterstellung.

Zunächst müssten Sie eine erstellen Verknüpfung zu Ihrem Vorgesetzten. Das Verknüpfen ist ebenfalls eine wichtige Technik: Wenn zwei Prozesse miteinander verknüpft sind und einer davon beendet wird, erhält ein anderer eine Benachrichtigung mit einem Beendigungsgrund. Wenn der verknüpfte Prozess abnormal beendet (dh abgestürzt) ist, wird auch sein Gegenstück beendet.

Dies kann mit den Funktionen spawn / 1 und spawn_link / 1 demonstriert werden:

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

In diesem Beispiel spawnen wir zwei Prozesse. Die innere Funktion wird erzeugt und mit dem aktuellen Prozess verknüpft. Wenn Sie nun einen Fehler in einem davon auslösen, wird auch ein anderer beendet:

spawn (fn -> IO.puts "hi from parent!") spawn_link (fn -> IO.puts "hi from child!" erhöhen ("oops.") end): timer.sleep (2000) IO.puts "nicht erreichbar! "end) # [error] Prozess #PID<0.83.0> hat eine Ausnahme # ** (RuntimeError) oops ausgelöst. # gen.ex: 5: anonymes Fn / 0 in: elixir_compiler_0 .__ DATEI __ / 1

Um einen Link zu erstellen, wenn Sie GenServer verwenden, ersetzen Sie einfach Ihren Start Funktionen mit start_link:

defmodule CalcServer verwendet GenServer def start_link (initial_value) und genServer.start_link (__ MODULE__, initial_value, name: __MODULE__) end #… end

Es geht ums Verhalten

Nun sollte natürlich ein Supervisor erstellt werden. Neues hinzufügen lib / calc_supervisor.ex Datei mit folgendem Inhalt:

defmodule CalcSupervisor verwenden Supervisor def start_link do Supervisor.start_link (__ MODULE__, nil) end def init (_) beaufsichtigen ([worker (CalcServer [0])], Strategie:: one_for_one) end end 

Hier ist viel los, bewegen wir uns also langsam.

start_link / 2 ist eine Funktion zum Starten des Supervisors. Beachten Sie, dass der entsprechende untergeordnete Prozess ebenfalls gestartet wird, sodass Sie keine Eingaben vornehmen müssen CalcServer.start_link (5) nicht mehr.

init / 2 ist ein Rückruf, der vorhanden sein muss, um das Verhalten einsetzen zu können. Das überwachen Funktion beschreibt im Grunde diesen Supervisor. Innerhalb legen Sie fest, welche Kindprozesse überwacht werden sollen. Wir spezifizieren natürlich die CalcServer Arbeitsprozess. [0] Hier bedeutet der Anfangszustand des Prozesses - es ist das gleiche wie das Sprichwort CalcServer.start_link (0).

:eins für eins ist der Name der Strategie zum Neustart des Prozesses (ähnlich einem berühmten Motto der Musketiere). Diese Strategie legt fest, dass nach Beendigung eines untergeordneten Prozesses ein neuer Prozess gestartet werden muss. Es gibt eine Handvoll anderer Strategien:

  • :einer für alle (noch mehr Musketier-Stil!) - Starten Sie alle Prozesse neu, wenn einer beendet wird.
  • : rest_for_one-Nach dem Beenden gestartete untergeordnete Prozesse werden neu gestartet. Der beendete Prozess wird ebenfalls neu gestartet.
  • : simple_one_for_one-Ähnlich wie: one_for_one, aber es muss nur ein untergeordneter Prozess in der Spezifikation vorhanden sein. Wird verwendet, wenn der überwachte Prozess dynamisch gestartet und gestoppt werden soll.

Die Gesamtidee ist also ganz einfach:

  • Zunächst wird ein Supervisor-Prozess gestartet. Das drin Callback muss eine Spezifikation zurückgeben, in der erläutert wird, welche Prozesse zu überwachen sind und wie Abstürze behandelt werden müssen.
  • Die überwachten Kindprozesse werden gemäß der Spezifikation gestartet.
  • Nach einem Absturz eines untergeordneten Prozesses werden die Informationen dank des eingerichteten Links an den Supervisor gesendet. Der Supervisor folgt dann der Neustartstrategie und führt die erforderlichen Aktionen aus.

Jetzt können Sie Ihr Programm erneut ausführen und versuchen, es durch Null zu teilen:

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

Der Zustand ist also verloren, aber der Prozess läuft, obwohl ein Fehler aufgetreten ist, was bedeutet, dass unser Supervisor gut funktioniert!

Dieser Kindprozess ist ziemlich kugelsicher, und Sie werden es buchstäblich schwer haben, ihn zu töten:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, ich bin unsterblich!

Beachten Sie jedoch, dass der Prozess technisch nicht neu gestartet wird. Stattdessen wird ein neuer Prozess gestartet, sodass die Prozess-ID nicht identisch ist. Im Grunde bedeutet dies, dass Sie Ihren Prozessen beim Start Namen geben sollten.

Die Anwendung

Es kann etwas umständlich sein, den Supervisor jedes Mal manuell zu starten. Zum Glück lässt sich das Problem mit dem Anwendungsmodul ganz einfach beheben. Im einfachsten Fall müssen Sie nur zwei Änderungen vornehmen.

Erstens, optimieren Sie die mix.exs Datei im Stammverzeichnis Ihres Projekts:

 #… Def application do # Geben Sie zusätzliche Anwendungen an, die von Erlang / Elixir verwendet werden sollen [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

Als nächstes schließen Sie die Anwendung Modul und stellen Sie den start / 2-Rückruf bereit, der automatisch ausgeführt wird, wenn Ihre App gestartet wird:

Defmodule CalcServer verwendet Application Use GenServer Def Start (_type, _args) für CalcSupervisor.start_link end #… end

Jetzt nach der Ausführung iex -S mix Kommando, Ihr Supervisor ist sofort einsatzbereit!

Unendlich startet neu?

Sie fragen sich vielleicht, was passiert, wenn der Prozess ständig abstürzt und der entsprechende Supervisor ihn erneut startet. Läuft dieser Zyklus unbegrenzt? Eigentlich nein. Nur standardmäßig 3 startet neu von innen 5 Sekunden sind erlaubt - nicht mehr. Wenn mehr Neustarts erfolgen, gibt der Supervisor auf und tötet sich und alle untergeordneten Prozesse. Hört sich schrecklich an, eh?

Sie können dies leicht überprüfen, indem Sie die folgende Codezeile immer und immer wieder ausführen (oder in einem Zyklus):

Process.whereis (CalcServer) |> Process.exit (: kill) #… # ** (BEENDEN von #PID<0.117.0>) ausschalten 

Es gibt zwei Optionen, die Sie anpassen können, um dieses Verhalten zu ändern:

  • : max_restarts-Wie viele Neustarts sind innerhalb des Zeitrahmens zulässig
  • : max_seconds-der tatsächliche Zeitrahmen

Beide Optionen sollten an die übergeben werden überwachen Funktion innerhalb der drin Ruf zurück:

 def init (_) überwache ([Worker (CalcServer, [0])], max_restarts: 5, max_seconds: 6, Strategie:: one_for_one) Ende

Fazit

In diesem Artikel haben wir über Elixir Supervisors gesprochen, mit denen wir untergeordnete Prozesse nach Bedarf überwachen und erneut starten können. Wir haben gesehen, wie sie Ihre Prozesse überwachen und bei Bedarf neu starten können, und wie Sie verschiedene Einstellungen anpassen, einschließlich Neustartstrategien und -frequenzen.

Hoffentlich fanden Sie diesen Artikel nützlich und interessant. Ich danke dir, dass du bei mir bleibst und bis zum nächsten Mal!