O GenServer (servidor genérico) é um comportamento que abstrai as interações cliente-servidor comuns entre processos do Elixir.
Lembras-te do ciclo de receção de quando aprendemos sobre processos? O comportamento GenServer fornece abstrações para implementar esses ciclos e para trocar mensagens com um processo que corre esse ciclo. Torna mais fácil manter estado e executar código assíncrono.
Tem atenção que o nome GenServer está carregado de significado. Também é usado para descrever um módulo que usa o comportamento GenServer, bem como um processo que foi 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 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 tua aplicação Elixir podem chamar para comunicar com este processo GenServer, como implementações de callbacks do servidor, que contêm a lógica deste GenServer.
Vamos ver primeiro um exemplo simples de um GenServer e depois descobrir o que significa cada callback.
Este é um servidor de exemplo que consegue responder às perguntas repetitivas de passageiros irritantes durante uma longa viagem de carro, mais precisamente à pergunta: «já chegámos?». Controla quantas vezes esta pergunta já foi feita, devolvendo 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
init/1Um servidor pode ser iniciado ao chamar GenServer.start/3 ou GenServer.start_link/3. Aprendemos a diferença entre essas funções no conceito de ligações.
Essas duas funções:
GenServer como primeiro argumento.init_arg. Como o nome sugere, este argumento é passado ao callback init/1.Iniciar um servidor ao chamar GenServer.start/3 ou GenServer.start_link/3 invoca o callback init/1 de forma bloqueante. O valor devolvido de init/1 determina se o servidor pode ser iniciado com êxito.
O callback init/1 devolve habitualmente um destes valores:
{:ok, state}. O servidor inicia o seu ciclo de receção 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 ciclo de receção. O processo termina com o motivo indicado.Existem também possibilidades mais avançadas que não vamos abordar agora.
Se o ciclo de receção do servidor arrancar, as funções GenServer.start/3 e GenServer.start_link/3 devolvem um tuplo {:ok, pid}. Caso contrário, devolvem {:error, reason}
handle_call/3Uma mensagem que exige uma resposta pode ser enviada a um processo servidor com GenServer.call/2. Esta 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 a mensagens síncronas. Recebe três argumentos:
message - o valor passado como segundo argumento a GenServer.call/2.from - o pid do processo que chama GenServer.call/2. Na maioria das vezes, este argumento pode ser ignorado.state - o estado atual do servidor. Lembra-te de que o seu valor inicial foi definido no callback init/1.O callback handle_call/3 devolve habitualmente um tuplo de 3 elementos, {:reply, reply, state}. Isto significa que o segundo elemento do tuplo, um reply que pode ser de qualquer tipo, é enviado de volta a quem chamou. O terceiro elemento do tuplo, state, é o novo estado do servidor depois de tratar esta mensagem.
Existem também possibilidades mais avançadas que não vamos abordar agora.
Para memorizares o que este callback faz pelo seu nome, pensa nele como «chamar» alguém ao telefone.
Se essa pessoa estiver disponível, recebes uma resposta imediatamente (de forma síncrona).
handle_cast/2Uma mensagem que não exige resposta pode ser enviada a um processo servidor com GenServer.cast/2. Os seus argumentos são idênticos aos de GenServer.call/2.
O callback handle_cast/2 é responsável por tratar essas mensagens. Recebe dois argumentos, message e state, que são os mesmos argumentos que no callback handle_call/3 (exceto from).
O callback handle_cast/2 devolve habitualmente um tuplo de 2 elementos, {:noreply, state}.
Existem também possibilidades mais avançadas que não vamos abordar agora.
Para memorizares o que este callback faz pelo seu nome, lembra-te de que «to cast» também significa «atirar».
Se atirares uma mensagem numa garrafa para o mar, não esperas receber uma resposta imediatamente, ou talvez nunca.
call ou cast?Usa quase sempre call, mesmo que o teu código de cliente não precise da resposta do servidor.
Usar call espera pela resposta, o que serve de mecanismo de contrapressão (para impedir que os clientes enviem demasiadas mensagens de uma só vez). Receber uma resposta do servidor é também a única forma de teres a certeza de que o servidor recebeu e tratou a mensagem do cliente.
handle_info/2As mensagens também podem acabar na caixa de entrada do servidor por outros meios que não a chamada a GenServer.call/2 ou GenServer.cast/2, por exemplo ao chamar a função simples send/2.
Para tratares dessas mensagens, usa o callback handle_info/2. Este callback funciona exatamente da mesma forma que handle_cast/2.
O comportamento GenServer fornece uma implementação abrangente de handle_info/2 que regista erros relativos a mensagens inesperadas. Se substituíres essa implementação predefinida, certifica-te de que incluis sempre a tua própria implementação abrangente. Se te esqueceres, o servidor vai abaixo se receber uma mensagem inesperada.
O valor devolvido de cada um dos quatro callbacks descritos acima pode ser estendido com mais um elemento do tuplo, um timeout. Por exemplo, em vez de devolveres {:ok, state} de init/1, devolve {:ok, state, timeout}.
O timeout pode ser usado para detetar a ausência de mensagens na caixa de correio durante um período específico. Se o servidor devolver um timeout a partir de um dos seus callbacks, e tiverem passado os milissegundos indicados sem chegar nenhuma mensagem, handle_info/2 é chamado com :timeout como primeiro argumento.