Ge

GenServer em Elixir

4 exercícios

Sobre GenServer

GenServer (servidor genérico) é um comportamento que abstrai as interações cliente-servidor comuns entre processos do Elixir.

Lembra do laço de recebimento de quando aprendemos sobre processos? O comportamento GenServer fornece abstrações para implementar esses laços e para trocar mensagens com um processo que executa esse laço. Isso facilita manter o estado e executar código assíncrono.

Note

Fique atento: o nome GenServer tem mais de um sentido. Ele também é usado para descrever um módulo que usa o comportamento GenServer, assim como um processo iniciado a partir de um módulo que usa o comportamento GenServer.

O comportamento GenServer define um callback obrigatório, init/1, e alguns callbacks opcionais interessantes: handle_call/3, handle_cast/2 e handle_info/3. Os clientes que usam um GenServer não devem chamar esses callbacks diretamente. Em vez disso, o módulo GenServer fornece funções que os clientes podem usar para se comunicar com um processo GenServer.

Muitas vezes, um único módulo define tanto uma API de cliente, um conjunto de funções que outras partes da sua aplicação Elixir podem chamar para se comunicar com esse processo GenServer, quanto implementações de callbacks do servidor, que contêm a lógica desse GenServer.

Vamos ver primeiro um exemplo simples de GenServer e depois entender o que cada callback faz.

Exemplo

Este é um exemplo de servidor que responde às perguntas repetitivas de passageiros irritantes durante uma longa viagem de carro, mais precisamente à pergunta: "já chegamos?". Ele acompanha quantas vezes essa pergunta foi feita e retorna respostas cada vez mais irritadas.

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

Callbacks

init/1

Um servidor pode ser iniciado chamando GenServer.start/3 ou GenServer.start_link/3. Aprendemos a diferença entre essas funções no conceito de links.

Essas duas funções:

  • Aceitam um módulo que implementa o comportamento GenServer como primeiro argumento.
  • Aceitam qualquer coisa como segundo argumento, chamado init_arg. Como o nome sugere, esse argumento é passado ao callback init/1.
  • Aceitam um terceiro argumento opcional com opções avançadas para rodar o processo, que não vamos abordar agora.

Iniciar um servidor chamando GenServer.start/3 ou GenServer.start_link/3 invoca o callback init/1 de forma bloqueante. O valor de retorno de init/1 determina se o servidor pode ser iniciado com sucesso.

O callback init/1 geralmente retorna um destes valores:

  • {:ok, state}. O servidor inicia o seu laço de recebimento usando state como estado inicial. state pode ser de qualquer tipo.
  • {:stop, reason}. reason pode ser de qualquer tipo. O servidor não inicia o seu laço de recebimento. O processo encerra com o motivo informado.

Também existem possibilidades mais avançadas que não vamos abordar agora.

Se o laço de recebimento do servidor iniciar, as funções GenServer.start/3 e GenServer.start_link/3 retornam uma tupla {:ok, pid}. Caso contrário, retornam {:error, reason}

handle_call/3

Uma mensagem que exige uma resposta pode ser enviada a um processo servidor com GenServer.call/2. Essa função espera o pid de um processo servidor em execução como primeiro argumento e a mensagem como segundo argumento. A mensagem pode ser de qualquer tipo.

O callback handle_call/3 é responsável por tratar e responder às mensagens síncronas. Ele recebe três argumentos:

  1. message: o valor passado como segundo argumento para GenServer.call/2.
  2. from: o pid do processo que chama GenServer.call/2. Na maioria das vezes, esse argumento pode ser ignorado.
  3. state: o estado atual do servidor. Lembre-se de que seu valor inicial foi definido no callback init/1.

O callback handle_call/3 geralmente retorna uma tupla de 3 elementos, {:reply, reply, state}. Isso significa que o segundo elemento da tupla, um reply que pode ser de qualquer tipo, será enviado de volta a quem chamou. O terceiro elemento da tupla, state, é o novo estado do servidor depois de tratar essa mensagem.

Também existem possibilidades mais avançadas que não vamos abordar agora.

Note

Para memorizar o que esse callback faz pelo nome, pense nele como "ligar" para alguém por telefone.

Se a pessoa estiver disponível, você recebe uma resposta imediatamente (de forma síncrona).

handle_cast/2

Uma mensagem que não exige resposta pode ser enviada a um processo servidor com GenServer.cast/2. Seus argumentos são idênticos aos de GenServer.call/2.

O callback handle_cast/2 é responsável por tratar essas mensagens. Ele recebe dois argumentos, message e state, que são os mesmos argumentos do callback handle_call/3 (exceto por from).

O callback handle_cast/2 geralmente retorna uma tupla de 2 elementos, {:noreply, state}.

Também existem possibilidades mais avançadas que não vamos abordar agora.

Note

Para memorizar o que esse callback faz pelo nome, lembre-se de que "to cast" também significa "to throw".

Se você joga uma mensagem numa garrafa ao mar, não espera receber uma resposta imediatamente, ou talvez nunca.

Devo usar call ou cast?

Quase sempre use call, mesmo que o seu código cliente não precise da resposta do servidor.

Usar call espera pela resposta, o que serve como mecanismo de contrapressão (para impedir que os clientes enviem mensagens demais de uma vez). Receber uma resposta do servidor também é a única forma de ter certeza de que o servidor recebeu e tratou a mensagem do cliente.

handle_info/2

As mensagens também podem chegar à caixa de entrada do servidor por outros meios que não sejam chamar GenServer.call/2 ou GenServer.cast/2, por exemplo chamar a própria função send/2.

Para tratar essas mensagens, use o callback handle_info/2. Esse callback funciona exatamente da mesma forma que handle_cast/2.

O comportamento GenServer fornece uma implementação de handle_info/2 que captura todas as mensagens e registra erros sobre mensagens inesperadas. Se você sobrescrever essa implementação padrão, certifique-se de sempre incluir a sua própria implementação que captura tudo. Se você esquecer, o servidor vai quebrar se receber uma mensagem inesperada.

Timeouts

O valor de retorno de cada um dos quatro callbacks descritos acima pode ser estendido com mais um elemento de tupla, um timeout. Por exemplo, em vez de retornar {:ok, state} de init/1, retorne {:ok, state, timeout}.

O timeout pode ser usado para detectar a ausência de mensagens na caixa de correio por um período específico. Se o servidor retornar um timeout de um de seus callbacks e o número especificado de milissegundos tiver passado sem que nenhuma mensagem chegue, handle_info/2 é chamado com :timeout como primeiro argumento.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda GenServer

A prática está bloqueada

Desbloqueie mais 2 exercícios para praticar GenServer