Nel mio precedente articolo di cui stavamo parlando Open Telecom Platform (OTP) e, più specificamente, l'astrazione GenServer che semplifica il lavoro con i processi del server. GenServer, come probabilmente ricorderai, è un comportamento-per usarlo, è necessario definire un modulo di callback speciale che soddisfi il contratto come dettato da questo comportamento.
Ciò che non abbiamo discusso, tuttavia, è gestione degli errori. Voglio dire, qualsiasi sistema può alla fine riscontrare errori ed è importante prenderli correttamente. È possibile fare riferimento all'articolo Come gestire le eccezioni in elisir per conoscere il provare / salvataggio
bloccare, aumentare
, e alcune altre soluzioni generiche. Queste soluzioni sono molto simili a quelle che si trovano in altri linguaggi di programmazione popolari, come JavaScript o Ruby.
Tuttavia, c'è di più su questo argomento. Dopotutto, Elixir è progettato per costruire sistemi concorrenti e fault-tolerant, quindi ha altre chicche da offrire. In questo articolo parleremo dei supervisori, che ci consentono di monitorare i processi e riavviarli dopo la loro conclusione. I supervisori non sono così complessi, ma piuttosto potenti. Possono essere facilmente modificati, configurati con varie strategie su come eseguire i riavvii e utilizzati negli alberi di supervisione.
Quindi oggi vedremo i supervisori in azione!
A scopo dimostrativo, utilizzeremo un codice di esempio del mio precedente articolo su GenServer. Questo modulo è chiamato CalcServer
, e ci consente di eseguire vari calcoli e di mantenere il risultato.
Va bene, quindi in primo luogo, creare un nuovo progetto usando il mescola nuovo calc_server
comando. Quindi, definire il modulo, includere GenServer
, e fornire il avviare / 1
scorciatoia:
# lib / calc_server.ex defmodule CalcServer utilizza GenServer def start (initial_value) do GenServer.start (__ MODULE__, initial_value, name: __MODULE__) end end
Quindi, fornire il init / 1
callback che verrà eseguito non appena il server viene avviato. Richiede un valore iniziale e utilizza una clausola di guardia per verificare se si tratta di un numero. In caso contrario, il server termina:
def init (initial_value) quando is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "Il valore deve essere un numero intero!" fine
Ora le funzioni di interfaccia del codice consentono di eseguire addizioni, divisioni, moltiplicazioni, calcoli della radice quadrata e il recupero del risultato (ovviamente è possibile aggiungere più operazioni matematiche in base alle esigenze):
def sqrt do GenServer.cast (__ MODULE__,: sqrt) end def add (numero) do GenServer.cast (__ MODULE__, : add, number) end def multiply (numero) do GenServer.cast (__ MODULE__, : moltiplicazione, numero ) end def div (numero) do GenServer.cast (__ MODULE__, : div, numero) end def result do GenServer.call (__ MODULE__,: result) end
La maggior parte di queste funzioni è gestita in modo asincrono, il che significa che non stiamo aspettando che si completino. L'ultima funzione è sincrono perché in realtà vogliamo aspettare che arrivi il risultato. Pertanto, aggiungere handle_call
e handle_cast
callback:
def handle_call (: result, _, state) do : reply, state, state end def handle_cast (operazione, stato) do case operation do: sqrt -> : noreply,: math.sqrt (stato) : multiply , moltiplicatore -> : noreply, stato * moltiplicatore : div, numero -> : noreply, stato / numero : aggiungi, numero -> : noreply, stato + numero _ -> : stop, "Non implementato", stato fine fine
Inoltre, specifica cosa fare se il server viene terminato (stiamo giocando a Captain Obvious qui):
def terminate (_reason, _state) do IO.puts "The server terminated" end
Il programma può ora essere compilato usando mix iS-S
e usato nel modo seguente:
CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875
Il problema è che il server si arresta in modo anomalo quando viene generato un errore. Ad esempio, prova a dividere per zero:
CalcServer.start (6.1) CalcServer.div (0) # [errore] GenServer CalcServer che termina # ** (ArithmeticError) argomento non valido nell'espressione aritmetica # (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 # Ultimo messaggio: : "$ gen_cast", : div, 0 # Stato: 6.1 CalcServer.result |> IO.puts # ** (uscita) chiuso in: GenServer.call (CalcServer,: result, 5000) # ** (ESCI ) nessun processo: il processo non è attivo o non vi è alcun processo attualmente associato al nome dato, probabilmente perché la sua applicazione non è iniziata # (elixir) lib / gen_server.ex: 729: GenServer.call/3
Quindi il processo è terminato e non può più essere utilizzato. Questo è davvero negativo, ma lo risolveremo molto presto!
Ogni linguaggio di programmazione ha i suoi idiomi, e così anche Elixir. Quando si ha a che fare con i supervisori, un approccio comune è quello di far arrestare un processo e quindi fare qualcosa al riguardo - probabilmente, riavviare e continuare.
Molti linguaggi di programmazione usano solo provare
e catturare
(o costrutti simili), che è uno stile di programmazione più difensivo. Stiamo essenzialmente cercando di anticipare tutti i possibili problemi e fornire un modo per superarli.
Le cose sono molto diverse con i supervisori: se un processo si blocca, si blocca. Ma il supervisore, proprio come un coraggioso medico di battaglia, è lì per aiutare a recuperare un processo caduto. Questo può sembrare un po 'strano, ma in realtà è una logica molto sana. Inoltre, puoi persino creare alberi di supervisione e in questo modo isolare gli errori, evitando che l'intera applicazione si arresti in modo anomalo se una delle sue parti presenta problemi.
Immagina di guidare una macchina: è composta da vari sottosistemi e non è possibile controllarli ogni volta. Quello che puoi fare è riparare un sottosistema se si rompe (o, beh, chiedere a un meccanico di macchina di farlo) e continuare il tuo viaggio. I supervisori in elisir fanno proprio questo: controllano i tuoi processi (indicati come processi figli) e riavvialo come necessario.
È possibile implementare un supervisore utilizzando il modulo comportamentale corrispondente. Fornisce funzioni generiche per la tracciabilità degli errori e il reporting.
Prima di tutto, dovresti creare un collegamento al tuo supervisore. Il collegamento è una tecnica abbastanza importante: quando due processi sono collegati insieme e uno di essi termina, un altro riceve una notifica con un motivo di uscita. Se il processo collegato termina in modo anomalo (ovvero, si blocca), anche la sua controparte esce.
Questo può essere dimostrato usando le funzioni spawn / 1 e spawn_link / 1:
spawn (fn -> IO.puts "ciao da genitore!" spawn_link (fn -> IO.puts "ciao da bambino!" fine) fine)
In questo esempio, stiamo generando due processi. La funzione interna viene generata e collegata al processo corrente. Ora, se si genera un errore in uno di essi, anche un altro terminerà:
spawn (fn -> IO.puts "ciao da genitore!" spawn_link (fn -> IO.puts "ciao da bambino!" raise ("oops.") end): timer.sleep (2000) IO.puts "irraggiungibile! "end) # [error] Process #PID<0.83.0> sollevato un'eccezione # ** (RuntimeError) oops. # gen.ex: 5: anonymous fn / 0 in: elixir_compiler_0 .__ FILE __ / 1
Quindi, per creare un collegamento quando si utilizza GenServer, è sufficiente sostituire il proprio inizio
funziona con start_link:
defmodule CalcServer usa GenServer def start_link (initial_value) do GenServer.start_link (__ MODULE__, initial_value, name: __MODULE__) end # ... end
Ora, ovviamente, dovrebbe essere creato un supervisore. Aggiungi un nuovo lib / calc_supervisor.ex file con il seguente contenuto:
defmodule CalcSupervisor usa Supervisor def start_link do Supervisor.start_link (__ MODULE__, nil) end def init (_) do supervise ([worker (CalcServer, [0])], strategy:: one_for_one) end end
C'è molto da fare qui, quindi muoviamoci lentamente.
start_link / 2 è una funzione per avviare il supervisore effettivo. Si noti che anche il processo figlio corrispondente verrà avviato, quindi non sarà necessario digitare CalcServer.start_link (5)
più.
init / 2 è un callback che deve essere presente per utilizzare il comportamento. Il sorvegliare
la funzione, in sostanza, descrive questo supervisore. All'interno si specificano i processi secondari da supervisionare. Stiamo, naturalmente, specificando il CalcServer
processo di lavoro. [0]
qui significa lo stato iniziale del processo, è lo stesso che dire CalcServer.start_link (0)
.
:uno per uno
è il nome della strategia di riavvio del processo (simile a un famoso motto dei Moschettieri). Questa strategia impone che quando un processo figlio termina, ne deve essere avviato uno nuovo. Ci sono una manciata di altre strategie disponibili:
:uno per tutti
(anche più stile Musketeer!) - riavvia tutti i processi se uno termina.: rest_for_one
-i processi figli sono stati avviati dopo il riavvio di quello terminato. Anche il processo terminato viene riavviato.: simple_one_for_one
-simile a: one_for_one ma richiede che un solo processo figlio sia presente nella specifica. Utilizzato quando il processo supervisionato deve essere avviato e arrestato dinamicamente.Quindi l'idea generale è abbastanza semplice:
dentro
la callback deve restituire una specifica che spiega quali processi monitorare e come gestire gli arresti anomali.Ora puoi eseguire nuovamente il tuo programma e provare a dividere per zero:
CalcSupervisor.start_link CalcServer.add (10) CalcServer.result # => 10 CalcServer.div (0) # => errore! CalcServer.result # => 0
Quindi lo stato viene perso, ma il processo è in esecuzione anche se si è verificato un errore, il che significa che il nostro supervisore sta funzionando correttamente!
Questo processo figlio è abbastanza a prova di proiettile, e letteralmente avremo difficoltà a ucciderlo:
Process.whereis (CalcServer) |> Process.exit (: kill) CalcServer.result # => 0 # HAHAHA, sono immortale!
Nota, tuttavia, che tecnicamente il processo non viene riavviato, piuttosto ne viene avviato uno nuovo, quindi l'id del processo non sarà lo stesso. Significa fondamentalmente che dovresti dare i nomi dei tuoi processi quando li inizi.
Potresti trovare un po 'noioso iniziare il supervisore manualmente ogni volta. Fortunatamente, è abbastanza facile da risolvere utilizzando il modulo Applicazione. Nel caso più semplice, dovrai solo apportare due modifiche.
In primo luogo, modificare il mix.exs file situato nella radice del tuo progetto:
# ... l'applicazione def fa # Specifica le applicazioni extra che userete da Erlang / Elixir [extra_applications: [: logger], mod: CalcServer, [] # <== add this line ] end
Quindi, includi il Applicazione
modulo e fornire il callback di avvio / 2 che verrà eseguito automaticamente all'avvio dell'app:
defmodule CalcServer usa Application use GenServer def start (_type, _args) do CalcSupervisor.start_link end # ... end
Ora dopo aver eseguito il mix iS-S
comando, il tuo supervisore sarà subito operativo!
Potresti chiederti cosa succederà se il processo si blocca costantemente e il supervisore corrispondente lo riavvia nuovamente. Questo ciclo verrà eseguito indefinitamente? Beh, in realtà no. Di default, solo 3
riavvia dentro 5
i secondi sono ammessi, non più di questo. Se si verificano ulteriori riavvii, il supervisore si arrende e uccide se stesso e tutti i processi figlio. Sembra orribile, eh?
Puoi facilmente controllarlo eseguendo rapidamente la seguente riga di codice più e più volte (o eseguendola in un ciclo):
Process.whereis (CalcServer) |> Process.exit (: kill) # ... # ** (ESCI da #PID<0.117.0>) spegnimento
Esistono due opzioni che è possibile modificare per modificare questo comportamento:
: max_restarts
-quanti riavvii sono consentiti nei tempi previsti: max_seconds
-il tempo realeEntrambe queste opzioni dovrebbero essere passate al sorvegliare
funzione all'interno del dentro
richiama:
def init (_) do supervise ([worker (CalcServer, [0])], max_restarts: 5, max_seconds: 6, strategia:: one_for_one) fine
In questo articolo, abbiamo parlato di Elixir Supervisor, che ci permettono di monitorare e riavviare i processi figli secondo necessità. Abbiamo visto come possono monitorare i processi e riavviarli secondo necessità e come modificare le varie impostazioni, incluse le strategie di riavvio e le frequenze.
Spero che tu abbia trovato questo articolo utile e interessante. Ti ringrazio per essere stato con me e fino alla prossima volta!