Handledare i Elixir

I min tidigare artikel pratade vi om Open Telecom Platform (OTP) och mer specifikt GenServer-abstraktionen som gör det enklare att arbeta med serverprocesser. GenServer, som du säkert kommer ihåg, är en beteende-För att använda den måste du definiera en speciell återuppringningsmodul som uppfyller kontraktet som dikterats av detta beteende.

Vad vi inte har diskuterat är emellertid felhantering. Jag menar, vilket system som helst kan så småningom uppleva fel, och det är viktigt att ta dem ordentligt. Du kan hänvisa till hur man hanterar undantag i Elixir-artikeln för att lära sig om prova / räddning blockera, höja, och några andra generiska lösningar. Dessa lösningar är mycket liknade de som finns i andra populära programmeringsspråk, som JavaScript eller Ruby. 

Det finns fortfarande mer på detta ämne. Elixir är trots allt konstruerat för att bygga samtidiga och feltoleranta system, så det har andra godsaker att erbjuda. I den här artikeln kommer vi att prata om handledare, som tillåter oss att övervaka processer och starta om dem efter att de upphör. Handledare är inte så komplexa, men ganska kraftfulla. De kan enkelt tweaked, inrättas med olika strategier om hur man utför omstart och används i övervaknings träd.

Så idag ser vi handledare i aktion!

förberedelser

För demonstrationsändamål ska vi använda några exempelkod från min tidigare artikel om GenServer. Den här modulen heter CalcServer, och det tillåter oss att utföra olika beräkningar och fortsätta resultatet.

Okej, först och främst, skapa ett nytt projekt med hjälp av blanda ny calc_server kommando. Definiera sedan modulen, inkludera GenServer, och tillhandahålla start / 1 genväg:

# lib / calc_server.ex defmodule CalcServer använder GenServer def start (initial_value) gör GenServer.start (__ MODULE__, initial_value, namn: __MODULE__) ändänden

Ange sedan init / 1 återuppringning som kommer att köras så fort servern är igång. Det tar ett initialvärde och använder en skyddsklausul för att kontrollera om det är ett nummer. Om inte, slutar servern:

def init (initial_value) när is_number (initial_value) gör : ok, initial_value slut def init (_) gör : stop, "Värdet måste vara ett heltal!" slutet

Nu kodgränssnitt funktioner för att utföra addition, division, multiplication, beräkning av kvadratroten och hämta resultatet (självklart kan du lägga till mer matematiska operationer efter behov):

 def sqrt gör GenServer.cast (__ MODULE__,: sqrt) slut def add (number) gör GenServer.cast (__ MODULE__, : add, number) slut def multiplicera (antal) gör GenServer.cast (__ MODULE__, : multiplicera, nummer ) slut def div (nummer) gör GenServer.cast (__ MODULE__, : div, nummer) slutresultatet gör GenServer.call (__ MODULE__,: result) avsluta

De flesta av dessa funktioner hanteras asynkront, vilket betyder att vi inte väntar på att de ska slutföra. Den senare funktionen är synkron eftersom vi faktiskt vill vänta på att resultatet kommer fram. Lägg därför till handle_call och handle_cast callbacks:

 def hand_call (: result, _ state) gör : svara, state, state slut def handle_cast (operation, state) gör falloperation gör: sqrt -> : noreply: math.sqrt (state) : multiply multiplikator -> : noreply, state * multiplikator : div, nummer -> : noreply, state / number : lägg till, nummer -> : noreply, state + number _ ->  stoppa, "Ej implementerad", state slutet slutet

Ange också vad du ska göra om servern avslutas (vi spelar Captain Obvious här):

 def terminate (_reason, _state) gör IO.puts "Slutet på serverns slut"

Programmet kan nu sammanställas med iex -S-blandning och används på följande sätt:

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

Problemet är att servern kraschar när ett fel uppkommer. Försök till exempel att dela upp med noll:

CalcServer.start (6.1) CalcServer.div (0) # [fel] GenServer CalcServer avslutar # ** (ArithmeticError) dåligt argument i aritmetiskt uttryck # (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 # Senast meddelande:  : "$ gen_cast", : div, 0 # Stat: 6.1 CalcServer.result |> IO.puts # ** (exit) avslutad i: GenServer.call (CalcServer,: result, 5000) # ** ) ingen process: processen är inte levande eller det finns ingen process som för tillfället är associerad med det angivna namnet, eventuellt eftersom applikationen inte startas # (elixir) lib / gen_server.ex: 729: GenServer.call/3

Så avslutas processen och kan inte användas längre. Det här är verkligen dåligt, men vi ska fixa det här verkligen snart!

Låt det krascha

Varje programmeringsspråk har sina idiom, och det gör Elixir också. När man arbetar med handledare är ett gemensamt förhållningssätt att låta en process krascha och sedan göra något åt ​​det - förmodligen starta om och fortsätt. 

Många programmeringsspråk används bara Prova och fånga (eller liknande konstruktioner), vilket är en mer defensiv utformning av programmering. Vi försöker i grunden förutse alla möjliga problem och ge ett sätt att övervinna dem. 

Saker är väldigt olika med handledare: om en process kraschar, kraschar den. Men handledaren, precis som en modig kampläkare, är där för att hjälpa en fallen process att återhämta sig. Det här låter lite konstigt, men i verkligheten är det en väldigt ren logik. Dessutom kan du till och med skapa övervaknings träd och på så sätt isolera fel som förhindrar att hela applikationen kraschar om en av dess delar upplever problem.

Tänk dig att köra bil: den består av olika delsystem, och du kan inte kontrollera dem varje gång. Vad du kan göra är att fixa ett delsystem om det bryts (eller, fråga en bilmekaniker att göra det) och fortsätt din resa. Handledare i Elixir gör just det: de övervakar dina processer (kallad barnprocesser) och starta om dem efter behov.

Skapa en handledare

Du kan implementera en handledare med motsvarande beteendemodul. Det ger generiska funktioner för felspårning och rapportering.

Först och främst skulle du behöva skapa en länk till din handledare. Koppling är också en viktig teknik: När två processer är länkade ihop och en av dem upphör, får en annan meddelande med utgångsskäl. Om den länkade processen avslutades onormalt (det vill säga kraschade), kommer också sin motsvarighet ut.

Detta kan demonstreras med hjälp av spawn / 1 och spawn_link / 1 funktionerna:

spawn (fn -> IO.puts "hej från förälder!" spawn_link (fn -> IO.puts "hej från barn!" slutet) slutet)

I detta exempel gyter vi två processer. Den inre funktionen skapas och kopplas till den aktuella processen. Nu, om du tar upp ett fel i en av dem, kommer en annan också att säga upp:

spawn (fn -> IO.puts "hej från förälder!" spawn_link (fn -> IO.puts "hej från barn!" höja ("oops.") slutet): timer.sleep (2000) IO.puts "unreachable! "slutet) # [fel] Process #PID<0.83.0> höjde ett undantag # ** (RuntimeError) oops. # gen.ex: 5: anonym fn / 0 i: elixir_compiler_0 .__ FIL __ / 1

Så, för att skapa en länk när du använder GenServer, ersätter du din Start funktioner med start_link:

defmodule CalcServer använder GenServer def start_link (initial_value) gör GenServer.start_link (__ MODULE__, initial_value, namn: __MODULE__) slutet # ... än

Det handlar om beteende

Nu är det självklart att en handledare ska skapas. Lägg till en ny lib / calc_supervisor.ex fil med följande innehåll:

defmodule CalcSupervisor använder Supervisor def start_link gör Supervisor.start_link (__ MODULE__, nil) slut def init (_) övervaka ([worker (CalcServer, [0])], strategi:: en_for_one) änden 

Det händer mycket här, så låt oss flytta i en långsam takt.

start_link / 2 är en funktion för att starta den verkliga handledaren. Observera att motsvarande barnprocess kommer att startas också, så du behöver inte skriva CalcServer.start_link (5) längre.

init / 2 är en återuppringning som måste vara närvarande för att kunna använda beteendet. De övervaka funktion beskriver i princip denna handledare. Innehållet anger du vilket barn som arbetar för att övervaka. Vi anger givetvis CalcServer arbetstagarprocessen. [0] här betyder processens initiala tillstånd - det är detsamma som att säga CalcServer.start_link (0).

: one_for_one är namnet på processen om omstart av strategin (som liknar ett känt Musketeers motto). Denna strategi dikterar att när en barnprocess slutar, bör en ny startas. Det finns en handfull andra strategier tillgängliga:

  • :en för alla (ännu mer Musketeer-stil!) - starta om alla processer om man avslutar.
  • : rest_for_one-barnprocesser startade efter det att den avslutade har startats om. Den avslutade processen startas också.
  • : simple_one_for_one-liknar: one_for_one men kräver endast ett barnprocess för att vara närvarande i specifikationen. Används när den övervakade processen ska startas och stoppas dynamiskt.

Så den övergripande tanken är ganska enkel:

  • För det första startas en handledarprocess. De i det återuppringning måste returnera en specifikation som förklarar vilka processer som ska övervakas och hur man hanterar kraschar.
  • De övervakade barnprocesserna startas enligt specifikationen.
  • Efter en barnprocess kraschar, skickas informationen till handledaren tack vare den etablerade länken. Handledare följer sedan omstartsstrategin och utför nödvändiga åtgärder.

Nu kan du köra ditt program igen och försöka dela upp med noll:

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

Så staten går förlorad, men processen körs trots att ett fel har hänt, vilket innebär att vår handledare fungerar bra!

Denna barnprocess är ganska kollisionsäker, och du kommer bokstavligen att ha svårt att döda det:

Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, jag är odödlig!

Observera dock att tekniken inte startas om på nytt - snarare startas en ny, så kommer processidentifikationen inte att vara densamma. Det innebär i grunden att du ska ge dina processer namn när du startar dem.

Ansökan

Det kan hända att du är lite tråkig att starta handledaren manuellt varje gång. Lyckligtvis är det ganska lätt att fixa med hjälp av applikationsmodulen. I det enklaste fallet behöver du bara göra två ändringar.

För det första, tweak the mix.exs fil som ligger i roten till ditt projekt:

 # ... def application do # Ange extra program du ska använda från Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end

Därefter, inkludera Ansökan modul och tillhandahålla start / 2 återuppringning som körs automatiskt när din app är igång:

defmodule CalcServer använder Application Använd GenServer def start (_type, _args) gör CalcSupervisor.start_link slutet # ... än

Nu efter att ha kört iex -S-blandning kommandot kommer din handledare att vara igång direkt!

Oändlig omstartar?

Du kanske undrar vad som kommer att hända om processen ständigt kraschar och motsvarande övervakare startar om det igen. Kommer denna cykel att köras på obestämd tid? Tja, faktiskt nej. Som standard är det bara 3 omstartar inom 5 sekunder är tillåtna - inte mer än det. Om mer omstart sker, ger handledaren upp och dödar sig själv och alla barnprocesser. Låter skrämmande, eh?

Du kan enkelt kontrollera det genom att snabbt springa följande kodrad om och om igen (eller göra det i en cykel):

Process.whereis (CalcServer) |> Process.exit (: kill) # ... # ** (EXIT från #PID<0.117.0>) stänga av 

Det finns två alternativ som du kan tweak för att ändra detta beteende:

  • : max_restarts-hur många omstart tillåts inom tidsramen
  • : max_seconds-den faktiska tidsramen

Båda dessa alternativ ska skickas till övervaka funktionen inuti i det ring tillbaka:

 def init (_) övervaka ([arbetare (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategy:: one_for_one) slut

Slutsats

I den här artikeln har vi pratat om Elixir Supervisors, som tillåter oss att övervaka och starta om barnprocesser efter behov. Vi har sett hur de kan övervaka dina processer och starta om dem efter behov, och hur du anpassar olika inställningar, inklusive omstartsstrategier och frekvenser.

Förhoppningsvis fann du den här artikeln användbar och intressant. Jag tackar dig för att du bodde hos mig och tills nästa gång!