Supervizorii din Elixir

În articolul meu precedent am vorbit despre Platforma Open Telecom (OTP) și, mai precis, abstracția GenServer care simplifică lucrul cu procesele de server. GenServer, după cum probabil vă aduceți aminte, este a comportament-pentru a le utiliza, trebuie să definiți un modul special de apel invers care satisface contractul așa cum este dictat de acest comportament.

Ceea ce nu am discutat, totuși, este eroare de manipulare. Vreau să spun că orice sistem ar putea să aibă erori în cele din urmă și este important să le luați în mod corespunzător. Puteți consulta articolul despre modul de tratare a excepțiilor din articolul Elixir pentru a afla despre încercați / salvare bloc, a ridica, și alte soluții generice. Aceste soluții sunt foarte asemănătoare cu cele găsite în alte limbi de programare populare, precum JavaScript sau Ruby. 

Totuși, există mai multe despre acest subiect. La urma urmei, Elixir este conceput pentru a construi sisteme compatibile și tolerante la erori, deci are și alte avantaje de oferit. În acest articol vom vorbi despre supervizori, care ne permit să monitorizăm procesele și să le repornim după terminarea lor. Supervizorii nu sunt atât de complexi, dar destul de puternici. Ele pot fi ușor modificate, înființate cu diverse strategii cu privire la modul de efectuare a repornirilor și utilizate în copacii de supraveghere.

Așa că astăzi vom vedea supraveghetorii în acțiune!

preparate

Pentru demonstrații, vom folosi un exemplu de cod din articolul meu despre GenServer. Se numește acest modul CalcServer, și ne permite să efectuăm calcule variate și să persistăm rezultatul.

În primul rând, creați un nou proiect folosind amestecați noul server calc_server comanda. Apoi, definiți modulul, includeți GenServer, și să furnizeze start / 1 Comandă rapidă:

# lib / calc_server.ex defmodule CalcServer nu folosesc GenServer def start (inițial_value) genServer.start (__ MODULE__, initial_value, nume: __MODULE__) end end

Apoi, furnizați init / 1 callback care va fi rulat imediat ce serverul este pornit. Este nevoie de o valoare inițială și utilizează o clauză de gardă pentru a verifica dacă este un număr. Dacă nu, serverul termină:

def_inumber (initial_value) atunci când este_number (initial_value) do : ok, initial_value sfarsit def init (_) do : stop, "Valoarea trebuie sa fie un intreg!

Acum funcțiile de interfață codifică pentru a efectua adăugarea, împărțirea, multiplicarea, calculul rădăcinii pătrate și extragerea rezultatului (desigur puteți adăuga mai multe operații matematice după cum este necesar):

 (sql) genServer.cast (__ MODULE__,: sqrt) end end add (număr) genServer.cast (__ MODULE__, : add, number) end def multiplicare (număr) genServer.cast (__ MODULE__, : ) end def div (număr) genServer.cast (__ MODULE__, : div, număr) final rezultat def genServer.call (__ MODULE__,: rezultat) sfârșit

Cele mai multe dintre aceste funcții sunt tratate asincronă, ceea ce înseamnă că nu le așteptăm să se încheie. Ultima funcție este sincronic pentru că de fapt dorim să așteptăm ca rezultatul să sosească. Prin urmare, adăugați handle_call și handle_cast callback:

 Definiți parametrul de execuție al parametrului de execuție al parametrului de execuție al parametrului de execuție al parametrului. , multiplicator -> : noreply, state * multiplicator : div, număr ->:: noreply, stop, "Nu este implementat", starea end end

De asemenea, specificați ce trebuie să faceți în cazul în care serverul este terminat (jucăm Captain Obvious aici):

 def terminate (_reason, _state) do IO.puts "Server terminat" sfârșitul

Programul poate fi acum compilat folosind iex -S mix și utilizate în modul următor:

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

Problema este că serverul se blochează atunci când apare o eroare. De exemplu, încercați să împărțiți cu zero:

CalcServer.start (6.1) CalcServer.div (0) # [eroare] GenServer CalcServer terminând # ** (ArithmeticError) argument rău în expresia aritmetică # (calc_server) lib / calc_server.ex: 44: CalcServer.handle_cast / 2 # ) 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 # : $ gen_cast, : div, 0 # Stare: 6.1 CalcServer.result |> IO.puts # ** (ieșire) ieșit din: GenServer.call (CalcServer,: result, 5000) # ** ) nici un proces: procesul nu este în viață sau nu există niciun proces asociat în prezent cu numele dat, posibil deoarece aplicația nu a fost pornită # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Deci procesul este terminat și nu mai poate fi folosit. Acest lucru este într-adevăr rău, dar vom rezolva acest lucru foarte curând!

Lasă-l să se prăbușească

Fiecare limbaj de programare are propriile idiomuri, la fel și Elixirul. Atunci când se ocupă de supraveghetori, o abordare comună este de a lăsa un proces de prăbușire și apoi de a face ceva despre el - probabil, reporniți și continuați. 

Multe limbi de programare utilizează numai încerca și captură (sau construcții similare), care este un stil mai defensiv de programare. În principiu, încercăm să anticipăm toate problemele posibile și să oferim o modalitate de a le depăși. 

Lucrurile sunt foarte diferite cu supraveghetorii: dacă un proces se prăbușește, acesta se blochează. Dar supraveghetorul, la fel ca un medic de luptă curajos, este acolo pentru a ajuta la recuperarea procesului căzut. Acest lucru poate părea un pic ciudat, dar în realitate aceasta este o logică foarte sănătoasă. În plus, puteți crea copaci de supraveghere și astfel puteți izola erorile, împiedicând întreaga aplicație să se prăbușească dacă una dintre părțile sale întâmpină probleme.

Imaginați-vă că conduceți o mașină: este compusă din diverse subsisteme și nu puteți să le verificați de fiecare dată. Ce puteți face este să reparați un subsistem în cazul în care acesta se rupe (sau, bine, cereți un mecanic auto să facă acest lucru) și continuați călătoria. Supervizorii din Elixir fac exact asta: ei monitorizează procesele dvs. (denumite în continuare procesele copilului) și reporniți-le după cum este necesar.

Crearea unui supervizor

Puteți implementa un supervizor folosind modulul de comportament corespunzător. Acesta oferă funcții generice pentru urmărirea și raportarea erorilor.

Mai întâi de toate, va trebui să creați o legătură pentru supraveghetorul tău. Legarea este, de asemenea, o tehnică destul de importantă: atunci când două procese sunt legate împreună și unul dintre ele se termină, altul primește notificarea cu un motiv de ieșire. Dacă procesul legat sa terminat anormal (adică, sa prăbușit), omologul său ieșea, de asemenea.

Acest lucru poate fi demonstrat folosind funcțiile spawn / 1 și spawn_link / 1:

spawn_link (fn -> IO.puts "hi de la copil!" sfârșit) sfârșitul)

În acest exemplu, noi lansăm două procese. Funcția interioară este generată și legată de procesul actual. Acum, dacă ridicați o eroare în unul dintre ele, altul se va termina și:

spawn_link (fn -> IO.puts "hi de la părinte!" spawn_link (fn -> IO.puts "hi from child!" "sfârșit) # [eroare] Procesul #PID<0.83.0> a ridicat o excepție # ** (RuntimeError) oops. # gen.ex: 5: anonim fn / 0 în: elixir_compiler_0 .__ FILE __ / 1

Deci, pentru a crea un link atunci când utilizați GenServer, pur și simplu înlocuiți-vă start funcționează cu start_link:

defmodule CalcServer folosesc GenServer def start_link (initial_value) genServer.start_link (__ MODULE__, initial_value, nume: __MODULE__) sfarsit # ... sfarsit

Este vorba despre comportament

Acum, desigur, ar trebui creat un supraveghetor. Adăugați un mesaj nou lib / calc_supervisor.ex fișier cu următorul conținut:

defmodule CalcSupervisor utilizează supervizorul def start_link do Supervisor.start_link (__ MODULE__, nil) end def init (_) supervizează ([worker (CalcServer, [0])], strategie:: one_for_one) end end 

Se întâmplă multe aici, așa că hai să mergem într-un ritm lent.

start_link / 2 este o funcție pentru a începe supraveghetorul real. Rețineți că procesul de copil corespunzător va fi pornit, de asemenea, deci nu va trebui să tastați CalcServer.start_link (5) mai.

init / 2 este un apel invers care trebuie să fie prezent pentru a folosi comportamentul. supraveghea funcția, în esență, descrie acest supraveghetor. În interiorul dvs. specificați ce procese copil să supravegheze. Desigur, specificăm CalcServer proces de muncă. [0] aici înseamnă starea inițială a procesului - este același lucru pe care îl spuneți CalcServer.start_link (0).

:unu pentru unu este numele strategiei de repornire a procesului (asemănător cu un motto faimos de muschetari). Această strategie dictează că, atunci când un proces copil se termină, trebuie început un nou proces. Există o serie de alte strategii disponibile:

  • :unul pentru toti (chiar mai mult stil Musketeer!) - reporniți toate procesele dacă se termină.
  • : rest_for_one-procesele copil au inceput dupa ce restul a fost repornit. Procesul terminat este repornit, de asemenea.
  • : simple_one_for_one-similar cu: one_for_one dar necesită doar un singur proces copil să fie prezent în caietul de sarcini. Se utilizează atunci când procesul supravegheat trebuie pornit dinamic și oprit.

Deci ideea generală este destul de simplă:

  • În primul rând, începe un proces de supraveghere. init apelul înapoi trebuie să returneze o specificație care explică ce procese să monitorizeze și cum să se ocupe de accidente.
  • Procesele copilului supravegheat sunt pornite conform specificațiilor.
  • După ce procesul copilului se blochează, informațiile sunt trimise supervizorului datorită legăturii stabilite. Supervizorul urmează strategia de repornire și efectuează acțiunile necesare.

Acum, puteți rula din nou programul și încercați să împărțiți cu zero:

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

Deci, statul se pierde, dar procesul se desfășoară chiar dacă sa produs o eroare, ceea ce înseamnă că supraveghetorul nostru funcționează bine!

Acest proces copil este destul de bulletproof, și literalmente va avea un timp greu de ucidere:

Process.where (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, sunt nemuritor!

Rețineți totuși că, din punct de vedere tehnic, procesul nu este repornit - mai degrabă se începe un proces nou, astfel încât id-ul procesului nu va fi același. În principiu, înseamnă că trebuie să dați numele proceselor când le porniți.

Aplicația

S-ar putea să vă simțiți oarecum obositoare pentru a porni supraveghetorul manual de fiecare dată. Din fericire, este destul de ușor să remediați utilizarea modulului Application. În cel mai simplu caz, va trebui doar să faceți două modificări.

În primul rând, tweak-ul mix.exs fișier situat în rădăcina proiectului dvs.:

 # ... Definiți aplicațiile pe care le veți folosi de la Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

Apoi, includeți cerere modul și furnizați apelul de start / 2 care va fi rulat automat când aplicația dvs. este pornită:

defmodule CalcServer nu se utilizează Utilizarea aplicației GenServer def start (_type, _args) nu se termină CalcSupervisor.start_link # ... end

Acum, după executarea iex -S mix comanda, supervizorul tău va apărea imediat!

Infinit Repornește?

S-ar putea să vă întrebați ce se va întâmpla dacă procesul se blochează constant și supraveghetorul corespunzător îl repornește din nou. Va dura acest ciclu pe termen nelimitat? De fapt, nu. În mod implicit, numai 3 repornește în interiorul 5 sunt permise câteva secunde - nu mai mult decât atât. Dacă se produc mai multe repornii, supraveghetorul renunță și se ucide pe sine și pe toate procesele copilului. Sună groaznic, eh?

Puteți să o verificați cu ușurință, executând repede următoarea linie de cod (sau efectuând-o într-un ciclu):

Process.whereis (CalcServer) |> Process.exit (: kill) # ... # ** (EXIT din #PID<0.117.0>) închide 

Există două opțiuni pe care le puteți modifica pentru a schimba acest comportament:

  • : max_restarts-câte restarte sunt permise în intervalul de timp
  • : max_seconds-intervalul real de timp

Ambele opțiuni trebuie transmise la supraveghea funcția în interiorul init suna inapoi:

 def init (_) supraveghează ([lucrător (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategie:: one_for_one)

Concluzie

În acest articol, am vorbit despre Elixir Supervisors, care ne permit să monitorizăm și să repornim procesele copilului după cum este necesar. Am văzut cum vă pot monitoriza procesele și le puteți reporni după cum este necesar și cum puteți modifica diferite setări, inclusiv strategiile și frecvențele de repornire.

Sperăm că ați găsit acest articol util și interesant. Vă mulțumesc că ați rămas cu mine și până la următoarea dată! 

Cod