Ge

GenServer in Elixir

4 esercizi

Informazioni su GenServer

GenServer (server generico) è un comportamento che astrae le comuni interazioni client-server tra i processi Elixir.

Ricordi il ciclo di ricezione di quando abbiamo parlato dei processi? Il comportamento GenServer fornisce astrazioni per implementare questo tipo di cicli e per scambiare messaggi con un processo che li esegue. Rende più facile mantenere lo stato ed eseguire codice asincrono.

Note

Attenzione: il nome GenServer è carico di significati. Si usa anche per descrivere un modulo che usa il comportamento GenServer, oltre che un processo avviato da un modulo che usa il comportamento GenServer.

Il comportamento GenServer definisce un callback obbligatorio, init/1, più alcuni callback opzionali interessanti: handle_call/3, handle_cast/2 e handle_info/3. I client che usano un GenServer non devono chiamare direttamente quei callback. Al contrario, il modulo GenServer fornisce delle funzioni che i client possono usare per comunicare con un processo GenServer.

Spesso un singolo modulo definisce sia una API del client, un insieme di funzioni che altre parti della tua app Elixir possono chiamare per comunicare con questo processo GenServer, sia le implementazioni dei callback del server, che contengono la logica di questo GenServer.

Vediamo prima un semplice esempio di GenServer, per poi scoprire cosa significa ciascun callback.

Esempio

Questo è un server di esempio che sa rispondere alle ripetitive insistenze di passeggeri fastidiosi durante un lungo viaggio in auto, o meglio, alla domanda: «siamo già arrivati?». Tiene traccia di quante volte è stata posta questa domanda, restituendo risposte sempre più infastidite.

defmodule AnnoyingPassengerAutoresponder do
  use GenServer
  # Client API

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end

  def are_we_there_yet?(pid) do
    GenServer.call(pid, :are_we_there_yet?)
  end

  # Server callbacks

  @impl GenServer
  def init(_init_arg) do
    # the initial count of questions asked is always 0
    state = 0
    {:ok, state}
  end

  @impl GenServer
  def handle_call(:are_we_there_yet?, _from, state) do
    reply =
      cond do
        state <= 3 -> "No."
        state <= 10 -> "I told you #{state} times already. No."
        true -> "..."
      end

    # increase the count of questions asked
    new_state = state + 1
    # reply to the caller
    {:reply, reply, new_state}
  end
end

Callback

init/1

Un server può essere avviato chiamando GenServer.start/3 o GenServer.start_link/3. Abbiamo scoperto la differenza tra queste due funzioni nel concetto dei link.

Queste due funzioni:

  • accettano come primo argomento un modulo che implementa il comportamento GenServer.
  • accettano come secondo argomento una cosa qualsiasi chiamata init_arg. Come suggerisce il nome, questo argomento viene passato al callback init/1.
  • accettano un terzo argomento opzionale con opzioni avanzate per l'esecuzione del processo, che non tratteremo adesso.

Avviare un server chiamando GenServer.start/3 o GenServer.start_link/3 invoca il callback init/1 in modo bloccante. Il valore restituito da init/1 determina se il server può essere avviato con successo.

Il callback init/1 di solito restituisce uno di questi valori:

  • {:ok, state}. Il server avvia il suo ciclo di ricezione usando state come stato iniziale. state può essere di qualsiasi tipo.
  • {:stop, reason}. reason può essere di qualsiasi tipo. Il server non avvia il suo ciclo di ricezione. Il processo termina con il motivo indicato.

Esistono anche possibilità più avanzate che non tratteremo adesso.

Se il ciclo di ricezione del server parte, le funzioni GenServer.start/3 e GenServer.start_link/3 restituiscono una tupla {:ok, pid}. Altrimenti restituiscono {:error, reason}.

handle_call/3

Un messaggio che richiede una risposta può essere inviato a un processo server con GenServer.call/2. Questa funzione si aspetta come primo argomento il pid di un processo server in esecuzione e come secondo argomento il messaggio. Il messaggio può essere di qualsiasi tipo.

Il callback handle_call/3 si occupa di gestire e rispondere ai messaggi sincroni. Riceve tre argomenti:

  1. message: il valore passato come secondo argomento a GenServer.call/2.
  2. from: il pid del processo che chiama GenServer.call/2. Il più delle volte questo argomento può essere ignorato.
  3. state: lo stato attuale del server. Ricorda che il suo valore iniziale è stato impostato nel callback init/1.

Il callback handle_call/3 di solito restituisce una tupla di 3 elementi, {:reply, reply, state}. Questo significa che il secondo elemento della tupla, un reply che può essere di qualsiasi tipo, viene rimandato a chi ha chiamato. Il terzo elemento della tupla, state, è il nuovo stato del server dopo aver gestito questo messaggio.

Esistono anche possibilità più avanzate che non tratteremo adesso.

Note

Per memorizzare dal nome cosa fa questo callback, pensa a quando «chiami» qualcuno al telefono.

Se quella persona è disponibile, ricevi subito una risposta (in modo sincrono).

handle_cast/2

Un messaggio che non richiede una risposta può essere inviato a un processo server con GenServer.cast/2. I suoi argomenti sono identici a quelli di GenServer.call/2.

Il callback handle_cast/2 si occupa di gestire quei messaggi. Riceve due argomenti, message e state, gli stessi del callback handle_call/3 (tranne from).

Il callback handle_cast/2 di solito restituisce una tupla di 2 elementi, {:noreply, state}.

Esistono anche possibilità più avanzate che non tratteremo adesso.

Note

Per memorizzare dal nome cosa fa questo callback, ricorda che «to cast» significa anche «gettare».

Se getti in mare un messaggio in bottiglia, non ti aspetti di ricevere subito una risposta, né forse mai.

Devo usare call o cast?

Usa quasi sempre call, anche se il codice del client non ha bisogno della risposta del server.

Usare call attende la risposta, che funge da meccanismo di contropressione (per evitare che i client inviino troppi messaggi tutti insieme). Ricevere una risposta dal server è anche l'unico modo per essere sicuri che il server abbia ricevuto e gestito il messaggio del client.

handle_info/2

I messaggi possono anche finire nella casella di posta del server per vie diverse dalla chiamata a GenServer.call/2 o GenServer.cast/2, per esempio chiamando la semplice funzione send/2.

Per gestire questi messaggi, usa il callback handle_info/2. Questo callback funziona esattamente come handle_cast/2.

Il comportamento GenServer fornisce un'implementazione di handle_info/2 che intercetta tutti i messaggi e registra errori relativi a messaggi inattesi. Se sovrascrivi quell'implementazione predefinita, assicurati di includere sempre anche la tua implementazione che intercetta tutti i messaggi. Se te ne dimentichi, il server andrà in crash quando riceve un messaggio inatteso.

Timeout

Il valore restituito da ciascuno dei quattro callback descritti sopra può essere esteso con un ulteriore elemento della tupla, un timeout. Per esempio, invece di restituire {:ok, state} da init/1, restituisci {:ok, state, timeout}.

Il timeout può essere usato per rilevare l'assenza di messaggi nella casella di posta per un certo periodo. Se il server restituisce un timeout da uno dei suoi callback e sono trascorsi i millisecondi specificati senza che sia arrivato alcun messaggio, handle_info/2 viene chiamato con :timeout come primo argomento.

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara GenServer

La pratica è bloccata

Sblocca 2 altri esercizi per esercitarti su GenServer