Supervisors in Elixir

In mijn vorige artikel hadden we het over Open Telecom Platform (OTP) en, meer specifiek, de GenServer-abstractie die het eenvoudiger maakt om met serverprocessen te werken. GenServer is, zoals u zich waarschijnlijk herinnert, een gedrag-om het te gebruiken, moet u een speciale callback-module definiëren die voldoet aan het contract zoals gedicteerd door dit gedrag.

Wat we echter niet hebben besproken, is dat wel foutafhandeling. Ik bedoel, elk systeem kan uiteindelijk fouten ervaren en het is belangrijk om ze op de juiste manier af te nemen. U kunt verwijzen naar het artikel Uitzonderingen behandelen in Elixir voor meer informatie over de probeer / rescue blok, verhogen, en enkele andere generieke oplossingen. Deze oplossingen komen sterk overeen met die in andere populaire programmeertalen, zoals JavaScript of Ruby. 

Toch is er meer aan dit onderwerp. Immers, Elixir is ontworpen om gelijktijdige en fouttolerante systemen te bouwen, dus het heeft nog andere dingen te bieden. In dit artikel zullen we het hebben over supervisors, die ons in staat stellen processen te controleren en ze opnieuw te starten nadat ze zijn beëindigd. Supervisors zijn niet zo complex, maar behoorlijk krachtig. Ze kunnen eenvoudig worden aangepast, worden opgezet met verschillende strategieën voor het opnieuw opstarten en worden gebruikt in supervisiestructuren.

Dus vandaag zien we supervisors in actie!

Voorbereidende werkzaamheden

Voor demonstratiedoeleinden gebruiken we enkele voorbeeldcodes uit mijn vorige artikel over GenServer. Deze module wordt genoemd CalcServer, en het stelt ons in staat om verschillende berekeningen uit te voeren en het resultaat te behouden.

Oké, dus maak eerst een nieuw project met behulp van de mix nieuwe calc_server commando. Definieer vervolgens de module, opnemen GenServer, en bied de start / 1 shortcut:

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

Geef vervolgens de init / 1 callback die wordt uitgevoerd zodra de server is gestart. Het heeft een initiële waarde en maakt gebruik van een bewakingsclausule om te controleren of het een nummer is. Als dit niet het geval is, wordt de server beëindigd:

def init (initial_value) wanneer is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "De waarde moet een geheel getal zijn!" einde

Voer nu codefunctiefuncties uit om optelling, verdeling, vermenigvuldiging, berekening van vierkantswortel uit te voeren en het resultaat op te halen (u kunt natuurlijk meer wiskundige bewerkingen toevoegen indien nodig):

 def sqrt do GenServer.cast (__ MODULE__,: sqrt) einde def add (nummer) do GenServer.cast (__ MODULE__, : add, number) end def multiply (number) do GenServer.cast (__ MODULE__, : vermenigvuldigen, aantal ) einde def div (nummer) do GenServer.cast (__ MODULE__, : div, number) end def result do GenServer.call (__ MODULE__,: result) end

De meeste van deze functies worden afgehandeld asynchroon, wat betekent dat we niet wachten tot ze klaar zijn. De laatste functie is synchrone omdat we eigenlijk willen wachten tot het resultaat arriveert. Voeg daarom toe handle_call en handle_cast callbacks:

 def handle_call (: result, _, state) do : reply, state, state end def handle_cast (operation, state) do case operatie do: sqrt -> : noreply,: math.sqrt (state) : vermenigvuldigen , vermenigvuldiger -> : noreply, state * multiplier : div, number -> : noreply, state / number : add, number -> : noreply, state + number _ -> : stop, "Niet geïmplementeerd", "state einde

Geef ook op wat u moet doen als de server wordt beëindigd (we spelen hier Captain Obvious):

 Def terminate (_reason, _state) do IO.puts "The server terminated" end

Het programma kan nu worden gecompileerd met behulp van iex -S mix en gebruikt op de volgende manier:

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

Het probleem is dat de server crasht wanneer een fout optreedt. Probeer bijvoorbeeld te delen door nul:

CalcServer.start (6.1) CalcServer.div (0) # [fout] GenServer CalcServer beëindigt # ** (ArithmeticError) foutargument in rekenkundige uitdrukking # (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 # Laatste bericht:  : "$ gen_cast", : div, 0 # State: 6.1 CalcServer.result |> IO.puts # ** (exit) verlaten in: GenServer.call (CalcServer,: result, 5000) # ** (EXIT ) Geen proces: het proces is niet actief of er is momenteel geen proces gekoppeld aan de opgegeven naam, mogelijk omdat de toepassing niet is gestart # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Het proces is dus beëindigd en kan niet meer worden gebruikt. Dit is inderdaad slecht, maar we gaan dit snel oplossen!

Let It Crash

Elke programmeertaal heeft zijn idioom, en Elixir ook. Bij het omgaan met supervisors is een algemene aanpak om een ​​proces te laten crashen en er vervolgens iets aan te doen - waarschijnlijk opnieuw opstarten en doorgaan. 

Veel programmeertalen alleen gebruikt proberen en vangst (of vergelijkbare constructies), wat een meer verdedigende manier van programmeren is. We proberen in feite alle mogelijke problemen te anticiperen en bieden een manier om ze te overwinnen. 

Het gaat heel anders met supervisors: als een proces vastloopt, crasht het. Maar de supervisor, net als een dappere strijddokter, is er om een ​​gevallen proces te helpen herstellen. Dit klinkt misschien een beetje vreemd, maar in werkelijkheid is dat een logische logica. Bovendien kunt u zelfs supervisiestructuren maken en op deze manier fouten isoleren, zodat de hele applicatie niet crasht als een van de onderdelen problemen ondervindt.

Stel u voor dat u een auto bestuurt: deze is samengesteld uit verschillende subsystemen en u kunt ze niet elke keer controleren. Wat je kunt doen is een subsysteem repareren als het breekt (of, nou ja, vraag het aan een automonteur om dit te doen) en je reis voortzetten. Supervisors in Elixir doen precies dat: zij controleren uw processen (aangeduid als kind processen) en herstart ze indien nodig.

Een supervisor maken

U kunt een supervisor implementeren met behulp van de bijbehorende gedragsmodule. Het biedt generieke functies voor foutopsporing en rapportage.

Allereerst zou je een link aan je leidinggevende. Het koppelen is ook een vrij belangrijke techniek: wanneer twee processen aan elkaar worden gekoppeld en een ervan wordt beëindigd, ontvangt een ander een melding met een reden voor afsluiten. Als het gekoppelde proces abnormaal is beëindigd (dat wil zeggen, gecrasht), wordt ook de tegenpartij afgesloten.

Dit kan worden aangetoond met de functies spawn / 1 en spawn_link / 1:

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

In dit voorbeeld brengen we twee processen voort. De innerlijke functie wordt voortgebracht en gekoppeld aan het huidige proces. Nu, als u een fout in een van hen opheft, wordt een andere ook beëindigd:

spawn (fn -> IO.puts "hi from parent!" spawn_link (fn -> IO.puts "hi from child!" raise ("oops.") end): timer.sleep (2000) IO.puts "onbereikbaar! "end) # [error] Verwerk #PID<0.83.0> heeft een uitzondering # ** (RuntimeError) opgehaald oops. # gen.ex: 5: anonymous fn / 0 in: elixir_compiler_0 .__ FILE __ / 1

Dus, om een ​​koppeling te maken wanneer u GenServer gebruikt, vervangt u eenvoudig uw begin functies met start_link:

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

Het draait allemaal om gedrag

Nu moet er natuurlijk een supervisor worden gemaakt. Voeg een nieuwe toe lib / calc_supervisor.ex bestand met de volgende inhoud:

defmodule CalcSupervisor gebruikt Supervisor def start_link do Supervisor.start_link (__ MODULE__, nihil) einde def init (_) supervisie ([worker (CalcServer, [0])], strategy:: one_for_one) end end 

Er gebeurt hier veel, dus laten we het langzaam doen.

start_link / 2 is een functie om de feitelijke supervisor te starten. Houd er rekening mee dat het bijbehorende onderliggende proces ook wordt gestart, zodat u niet hoeft te typen CalcServer.start_link (5) meer.

init / 2 is een callback die aanwezig moet zijn om het gedrag te gebruiken. De toezicht houden functie beschrijft in principe deze supervisor. Binnenin geeft u aan welk kind moet worden gecontroleerd. We specificeren natuurlijk het CalcServer werkproces. [0] hier betekent de initiële toestand van het proces - het is hetzelfde als zeggen CalcServer.start_link (0).

:een voor een is de naam van de proces-herstartstrategie (die lijkt op een beroemd motto van de Musketeers). Deze strategie schrijft voor dat wanneer een kinderproces wordt beëindigd, er een nieuw proces moet worden gestart. Er zijn een handvol andere strategieën beschikbaar:

  • :een voor allen (zelfs meer Musketier-stijl!) - herstart alle processen als er een wordt beëindigd.
  • : rest_for_one-onderliggende processen zijn gestart nadat de beëindigde opnieuw is gestart. Het beëindigde proces wordt ook opnieuw gestart.
  • : simple_one_for_one-vergelijkbaar met: one_for_one maar vereist dat er slechts één onderliggende proces aanwezig is in de specificatie. Wordt gebruikt wanneer het bewaakte proces dynamisch moet worden gestart en gestopt.

Dus het algemene idee is vrij eenvoudig:

  • Allereerst wordt een supervisorproces gestart. De in het callback moet een specificatie retourneren waarin wordt uitgelegd welke processen moeten worden gecontroleerd en hoe crashes moeten worden afgehandeld.
  • De begeleide kinderprocessen worden gestart volgens de specificatie.
  • Nadat een kindproces is vastgelopen, wordt de informatie verzonden naar de supervisor dankzij de vastgestelde koppeling. Supervisor volgt dan de herstartstrategie en voert de nodige acties uit.

Nu kunt u uw programma opnieuw uitvoeren en proberen te delen door nul:

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

Dus de status is verloren, maar het proces loopt zelfs als er een fout is opgetreden, wat betekent dat onze supervisor goed werkt!

Dit kinderproces is behoorlijk kogelvrij en je zult het letterlijk moeilijk vinden om het te doden:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, ik ben onsterfelijk!

Houd er echter rekening mee dat het proces technisch niet opnieuw wordt gestart, maar dat er een nieuwe wordt gestart, dus de proces-ID zal niet hetzelfde zijn. Het betekent in feite dat je de namen van je processen moet geven wanneer je ze opstart.

De applicatie

Misschien vind je het een beetje saai om de supervisor elke keer handmatig te starten. Gelukkig is het vrij eenvoudig op te lossen door de applicatiemodule te gebruiken. In het eenvoudigste geval hoeft u slechts twee wijzigingen aan te brengen.

Ten eerste, tweak de mix.exs bestand in de hoofdmap van uw project:

 # ... def application do # Geef extra toepassingen op die u gebruikt van Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

Voeg vervolgens de Toepassing module en geef de start / 2 callback die automatisch wordt uitgevoerd wanneer uw app wordt gestart:

defmodule CalcServer gebruikt gebruik Applicatie GenServer def start (_type, _args) do CalcSupervisor.start_link end # ... end

Nu na het uitvoeren van de iex -S mix opdracht, uw supervisor is meteen klaar voor gebruik!

Oneindige herstarts?

Je kunt je afvragen wat er gaat gebeuren als het proces voortdurend crasht en de bijbehorende supervisor het opnieuw opstart. Zal deze cyclus oneindig doorgaan? Nou ja, eigenlijk, nee. Alleen standaard 3 start opnieuw binnen 5 seconden zijn toegestaan ​​- niet meer dan dat. Als er meer herstarts gebeuren, geeft de supervisor op en doodt zichzelf en alle onderliggende processen. Klinkt vreselijk, hè?

U kunt het eenvoudig controleren door de volgende regel code telkens opnieuw (of in een cyclus) uit te voeren:

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

Er zijn twee opties die u kunt aanpassen om dit gedrag te wijzigen:

  • : max_restarts-hoeveel herstarts zijn toegestaan ​​binnen het tijdsbestek
  • : max_seconds-het werkelijke tijdsbestek

Beide opties moeten worden doorgegeven aan de toezicht houden functie binnen de in het Bel terug:

 def init (_) do supervise ([worker (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategy:: one_for_one) end

Conclusie

In dit artikel hebben we het gehad over Elixir Supervisors, die ons in staat stellen om kindprocessen waar nodig te controleren en opnieuw te starten. We hebben gezien hoe ze uw processen kunnen bewaken en ze indien nodig kunnen herstarten, en hoe verschillende instellingen kunnen worden aangepast, inclusief herstartstrategieën en -frequenties.

Hopelijk vond je dit artikel nuttig en interessant. Ik dank u dat u bij mij bent gebleven en tot de volgende keer!