Tilsynsførere i Elixir

I min forrige artikkel snakket vi om Open Telecom Platform (OTP) og nærmere bestemt GenServer-abstraksjonen som gjør det enklere å arbeide med serverprosesser. GenServer, som du sikkert husker, er a oppførsel-For å bruke det, må du definere en spesiell tilbakeringingsmodul som tilfredsstiller kontrakten som diktert av denne oppførselen.

Det vi ikke har diskutert, er imidlertid feilhåndtering. Jeg mener, noe system kan til slutt oppleve feil, og det er viktig å ta av dem riktig. Du kan se hvordan du håndterer unntak i Elixir-artikkelen for å lære om prøve / redning blokkere, heve, og noen andre generiske løsninger. Disse løsningene er svært lik dem som finnes i andre populære programmeringsspråk, som JavaScript eller Ruby. 

Likevel er det mer til dette emnet. Tross alt er Elixir designet for å bygge samtidige og feiltolerante systemer, så det har andre godbiter å tilby. I denne artikkelen vil vi snakke om veiledere, som tillater oss å overvåke prosesser og starte dem på nytt etter at de avsluttes. Tilsynsførere er ikke så komplekse, men ganske kraftige. De kan enkelt tweaked, sette opp med ulike strategier for hvordan å utføre omstart, og brukes i tilsyn trær.

Så i dag ser vi veiledere i aksjon!

Forberedelser

For demonstrasjonsformål skal vi bruke noen prøvekode fra min tidligere artikkel om GenServer. Denne modulen kalles CalcServer, og det tillater oss å utføre ulike beregninger og vedvare resultatet.

Ok, for det første, opprett et nytt prosjekt ved hjelp av bland ny calc_server kommando. Definer deretter modulen, inkludere GenServer, og gi start / 1 snarvei:

# lib / calc_server.ex defmodule CalcServer bruker GenServer def start (initial_value) gjør GenServer.start (__ MODULE__, initial_value, navn: __MODULE__) slutten

Deretter gir du init / 1 tilbakeringing som vil bli kjørt så snart serveren er startet. Den tar en innledende verdi og bruker en vaktklausul for å sjekke om det er et nummer. Hvis ikke, avslutter serveren:

def init (initial_value) når is_number (initial_value) gjør : ok, initial_value avslutte def init (_) gjør : stop, "Verdien må være et heltall!"

Nå kodes grensesnitt funksjoner for å utføre tillegg, divisjon, multiplikasjon, beregning av kvadratroten, og hente resultatet (selvfølgelig kan du legge til flere matematiske operasjoner etter behov):

 def sqrt gjør GenServer.cast (__ MODULE__,: sqrt) ende def add (tall) gjør GenServer.cast (__ MODULE__, : add, number) ende def multiplisere (tall) gjør GenServer.cast (__ MODULE__, : multiply, number ) ende def div (nummer) gjør GenServer.cast (__ MODULE__, : div, nummer) ende def resultat gjør GenServer.call (__ MODULE__,: result) end

De fleste av disse funksjonene håndteres asynkront, noe som betyr at vi ikke venter på at de skal fullføre. Sistnevnte funksjon er synkron fordi vi faktisk ønsker å vente på at resultatet skal ankomme. Legg derfor til handle_call og handle_cast callbacks:

 def act_call (: result, _ state) gjør : svar, state, state ende def handle_cast (operasjon, state) gjør saksoperasjon gjør: sqrt -> : noreply: math.sqrt , multiplikator -> : noreply, state * multiplikator : div, nummer -> : noreply, state / number stopp, "Ikke implementert", state slutten

Angi også hva du skal gjøre hvis serveren avsluttes (vi spiller Captain Obvious her):

 def terminate (_reason, _state) gjør IO.puts "Serveren avsluttet" slutten

Programmet kan nå kompileres ved hjelp av iex -S blanding og brukes på følgende måte:

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

Problemet er at serveren krasjer når det oppstår en feil. For eksempel, prøv å dele med null:

CalcServer.start (6.1) CalcServer.div (0) # [feil] GenServer CalcServer avslutter # ** (ArithmeticError) dårlig argument i aritmetisk uttrykk # (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 # Siste melding:  : "$ gen_cast", : div, 0 # Stat: 6.1 CalcServer.result |> IO.puts # ** (exit) avsluttet i: GenServer.call (CalcServer,: result, 5000) # ** ) ingen prosess: prosessen er ikke i live eller det er ingen prosess som for øyeblikket er knyttet til det oppgitte navnet, muligens fordi applikasjonen ikke er startet # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Så prosessen er avsluttet og kan ikke brukes lenger. Dette er virkelig dårlig, men vi skal løse dette veldig snart!

La det krasje

Hvert programmeringsspråk har sine idiomer, og det samme gjør Elixir. Når du arbeider med veiledere, er en felles tilnærming å la en prosesskrasj og deretter gjøre noe med det - sannsynligvis, start på nytt og fortsett. 

Mange programmeringsspråk bruker bare prøve og å fange (eller lignende konstruksjoner), som er en mer defensiv stil med programmering. Vi prøver i utgangspunktet å forutse alle mulige problemer og gi en måte å overvinne dem på. 

Ting er svært forskjellige med veiledere: Hvis en prosess krasjer, krasjer den. Men veilederen, akkurat som en modig kampmedisin, er der for å hjelpe en fallen prosess gjenopprette. Dette høres kanskje litt rart ut, men i virkeligheten er det en veldig ren logikk. Dessuten kan du til og med opprette tilsynstrener og på denne måten isolere feil, slik at hele applikasjonen ikke krasjer hvis en av delene opplever problemer.

Tenk deg å kjøre bil: den består av ulike delsystemer, og du kan ikke sjekke dem hver gang. Hva du kan gjøre er å fikse et delsystem hvis det bryter (eller vel, spør en bilmekaniker å gjøre det) og fortsett reisen din. Tilsynsførere i Elixir gjør nettopp det: de overvåker prosessene dine (referert til som barn prosesser) og omstart dem etter behov.

Opprette en veileder

Du kan implementere en veileder ved hjelp av tilhørende opptaksmodul. Det gir generiske funksjoner for feilsøking og rapportering.

Først og fremst må du opprette en link til din veileder. Kobling er også en viktig teknikk: Når to prosesser er koblet sammen og en av dem slutter, mottar en annen melding med en avslutningsgrunn. Hvis den koblede prosessen avsluttet unormalt (det er krasjet), kommer også sin motpart ut.

Dette kan demonstreres ved hjelp av spawn / 1 og spawn_link / 1-funksjonene:

gyte (fn -> IO.puts "hei fra foreldre!" spawn_link (fn -> IO.puts "hei fra barn!" slutten) ende)

I dette eksemplet gyter vi to prosesser. Den indre funksjonen oppstår og knyttes til den nåværende prosessen. Nå, hvis du reiser en feil i en av dem, vil en annen også si opp:

(fn -> IO.puts "hei fra foreldre!" spawn_link (fn -> IO.puts "hei fra barn!" heve ("oops.") slutt): timer.sleep (2000) IO.puts "unreachable! "slutt) # [feil] Prosess #PID<0.83.0> hevet et unntak # ** (RuntimeError) oops. # gen.ex: 5: anonym fn / 0 i: elixir_compiler_0 .__ FIL __ / 1

Så, for å opprette en kobling når du bruker GenServer, erstatt du bare start Fungerer med start_link:

defmodule CalcServer bruker GenServer def start_link (initial_value) gjør GenServer.start_link (__ MODULE__, initial_value, navn: __MODULE__) ende # ... end

Det handler om atferd

Nå skal selvfølgelig en veileder opprettes. Legg til en ny lib / calc_supervisor.ex fil med følgende innhold:

defmodule CalcSupervisor bruker Supervisor def start_link gjør Supervisor.start_link (__ MODULE__, null) ende def init (_) overvåker ([worker (CalcServer, [0])], strategi:: en_for_one) ende 

Det skjer mye her, så la oss bevege seg i et lavt tempo.

start_link / 2 er en funksjon for å starte den faktiske veilederen. Legg merke til at den tilsvarende barnprosessen også vil starte, så du må ikke skrive CalcServer.start_link (5) lenger.

init / 2 er en tilbakeringing som må være tilstede for å kunne bruke oppførselen. De overvåke Funksjonen beskriver i utgangspunktet denne veilederen. På innsiden angir du hvilket barn som skal behandle. Vi oppgir selvfølgelig CalcServer arbeidsprosess. [0] her betyr prosessens første tilstand - det er det samme som å si CalcServer.start_link (0).

:en for en er navnet på prosessen omstartsstrategi (ligner et kjent Musketeers motto). Denne strategien dikterer at når en barneprosess avsluttes, skal en ny startes. Det finnes en håndfull andre strategier:

  • :en for alle (enda mer Musketeer-stil!) - Start alle prosessene på nytt hvis en avsluttes.
  • : rest_for_one-Barnprosesser startet etter at den avsluttede er startet på nytt. Den avsluttede prosessen starter også.
  • : simple_one_for_one-ligner på: one_for_one, men krever at bare ett barnprosess skal være til stede i spesifikasjonen. Brukes når overvåkingsprosessen skal startes og stoppes dynamisk.

Så den generelle ideen er ganske enkel:

  • For det første startes en veilederprosess. De i det tilbakeringing må returnere en spesifikasjon som forklarer hvilke prosesser som skal overvåkes og hvordan man håndterer krasjer.
  • De behandlede barnprosessene startes i henhold til spesifikasjonen.
  • Etter at en barneprosess krasjer, sendes informasjonen til veilederen takket være den etablerte lenken. Tilsynsføreren følger deretter omstartsstrategien og utfører de nødvendige tiltakene.

Nå kan du kjøre programmet igjen og prøve å dele med null:

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

Så staten går tapt, men prosessen kjører, selv om det har skjedd en feil, noe som betyr at vår veileder fungerer bra!

Denne barneprosessen er ganske bulletproof, og du vil bokstavelig talt ha det vanskelig å drepe det:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, jeg er udødelig!

Vær imidlertid oppmerksom på at prosessen ikke starter på nytt teknisk-men en ny blir startet, så prosess-ID-en vil ikke være den samme. Det betyr i utgangspunktet at du skal gi prosessene dine navn når du starter dem.

Søknaden

Det kan hende du finner det litt kjedelig å starte veilederen manuelt hver gang. Heldigvis er det ganske enkelt å fikse ved hjelp av applikasjonsmodulen. I det enkleste tilfellet trenger du bare å gjøre to endringer.

For det første, tweak the mix.exs fil som ligger i roten til prosjektet ditt:

 # ... def søknad # Angi ekstra applikasjoner du vil bruke fra Erlang / Elixir [ekstra_applikasjoner: [: logger], mod: CalcServer, [] # <== add this line ] end

Deretter inkluderer du applikasjon modul og gi start / 2 tilbakeringing som vil bli kjørt automatisk når appen din er startet:

defmodule CalcServer bruker applikasjonsbruk GenServer def start (_type, _args) gjør CalcSupervisor.start_link ende # ... end

Nå etter å ha utført iex -S blanding kommando, din veileder vil være oppe og kjører med en gang!

Uendelig Restarts?

Du lurer kanskje på hva som skal skje hvis prosessen hele tiden krasjer, og den tilsvarende veilederen starter det på nytt. Vil denne syklusen løpe på ubestemt tid? Vel, faktisk, nei. Som standard, bare 3 starter på nytt 5 sekunder er tillatt - ikke mer enn det. Hvis flere starter på nytt, gir veileder opp og dreper seg selv og hele barnet prosesser. Høres forferdelig, eh?

Du kan enkelt sjekke det ved å raskt kjøre følgende linje kode igjen og igjen (eller gjøre det i en syklus):

Process.whereis (CalcServer) |> Process.exit (: kill) # ... # ** (EXIT fra #PID<0.117.0>) skru av 

Det er to alternativer du kan justere for å endre denne virkemåten:

  • : max_restarts-hvor mange omstart er tillatt innen tidsrammen
  • : max_seconds-den faktiske tidsrammen

Begge disse alternativene skal sendes til overvåke fungere inne i i det Ring tilbake:

 def init (_) overvåker ([arbeider (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategi:: en_for_one) slutten

Konklusjon

I denne artikkelen har vi snakket om Elixir Supervisors, som tillater oss å overvåke og starte opp barneprosesser etter behov. Vi har sett hvordan de kan overvåke prosessene dine og starte dem på nytt etter behov, og hvordan du kan justere ulike innstillinger, inkludert omstartsstrategier og frekvenser.

Forhåpentligvis fant du denne artikkelen nyttig og interessant. Jeg takker for at du bodde hos meg og til neste gang!