GenServer (servidor genérico) é um comportamento que abstrai interações comuns cliente-servidor entre processos de 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 o estado e executar código assíncrono.
Atenção que o nome GenServer está carregado. 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 de servidor, que contêm a lógica deste GenServer.
Vamos ver primeiro um exemplo simples de um GenServer e depois aprender o que significa cada callback.
Este é um exemplo de servidor que consegue responder às inquisições repetitivas de passageiros irritantes durante uma longa viagem de carro, mais exatamente à pergunta: "já chegámos?". Mantém o registo de quantas vezes esta pergunta foi feita e devolve 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 chamando GenServer.start/3 ou GenServer.start_link/3. Aprendemos sobre a diferença entre essas funções no conceito de links.
Estas duas funções:
GenServer como primeiro argumento.init_arg. Como o nome sugere, este argumento é passado ao callback init/1.Iniciar um servidor chamando 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 sucesso.
O callback init/1 devolve normalmente 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 dos casos, 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 normalmente 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 para 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 uma 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 que no callback handle_call/3 (exceto from).
O callback handle_cast/2 devolve normalmente 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 como mecanismo de contrapressão (para impedir que os clientes enviem demasiadas mensagens de uma 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 meios que não sejam chamar GenServer.call/2 ou GenServer.cast/2, por exemplo chamando a função simples send/2.
Para tratar essas mensagens, usa o callback handle_info/2. Este callback funciona exatamente da mesma forma que o handle_cast/2.
O comportamento GenServer fornece uma implementação abrangente de handle_info/2 que regista erros sobre 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 rebentar 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 tempo limite. Por exemplo, em vez de devolveres {:ok, state} de init/1, devolve {:ok, state, timeout}.
O tempo limite 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 tempo limite de um dos seus callbacks, e tiverem passado os milissegundos especificados sem chegar nenhuma mensagem, handle_info/2 é chamado com :timeout como primeiro argumento.
A máquina Take-A-Number básica estava a vender muito bem, mas alguns utilizadores queixavam-se da falta de funcionalidades avançadas em comparação com outros modelos disponíveis no mercado.
O fabricante ouviu o feedback dos utilizadores e decidiu lançar um modelo de luxo com mais funcionalidades, e, mais uma vez, coube-te a tarefa de escrever o software desta máquina.
As novas funcionalidades do modelo de luxo incluem:
A lógica de negócio da máquina já foi implementada pelo teu colega e encontra-se no módulo TakeANumberDeluxe.State. Agora, a tua tarefa é envolvê-la num GenServer.
Usa o comportamento GenServer no módulo TakeANumberDeluxe.
Implementa a função start_link/1 e o callback GenServer necessário.
O argumento passado a start_link/1 é uma keyword list. Contém as chaves :min_number e :max_number. Os valores dessas chaves têm de ser passados à função TakeANumberDeluxe.State.new/2.
Se TakeANumberDeluxe.State.new/2 devolver um tuplo {:ok, state}, a máquina deve iniciar-se, usando o estado devolvido como seu estado. Se, em vez disso, devolver um tuplo {:error, error}, a máquina deve parar, indicando o erro devolvido como motivo da paragem.
TakeANumberDeluxe.start_link(min_number: 1, max_number: 9)
# => {:ok, #PID<0.174.0>}
TakeANumberDeluxe.start_link(min_number: 9, max_number: 1)
# => {:error, :invalid_configuration}
Talvez tenhas reparado que a função TakeANumberDeluxe.State.new/2 também recebe um terceiro argumento opcional, auto_shutdown_timeout. Vamos usá-lo no último passo deste exercício.
Implementa a função report_state/1 e o callback GenServer necessário. A máquina deve responder a quem a invoca com o seu estado atual.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.report_state(machine)
# => %TakeANumberDeluxe.State{
# max_number: 10,
# min_number: 1,
# queue: %TakeANumberDeluxe.Queue{in: [], out: []},
# auto_shutdown_timeout: :infinity,
# }
Implementa a função queue_new_number/1 e o callback GenServer necessário.
Deve chamar a função TakeANumberDeluxe.State.queue_new_number/1 com o estado atual da máquina.
Se TakeANumberDeluxe.State.queue_new_number/1 devolver um tuplo {:ok, new_number, new_state}, a máquina deve responder a quem a invoca com o novo número e definir o novo estado como seu estado. Se, em vez disso, devolver um tuplo {:error, error}, a máquina deve responder a quem a invoca com o erro e não alterar o seu estado.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 2)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 2}
TakeANumberDeluxe.queue_new_number(machine)
# => {:error, :all_possible_numbers_are_in_use}
Implementa a função serve_next_queued_number/2 e o callback GenServer necessário.
Deve chamar a função TakeANumberDeluxe.State.serve_next_queued_number/2 com o estado atual da máquina e o seu segundo argumento opcional, priority_number.
Se TakeANumberDeluxe.State.serve_next_queued_number/2 devolver um tuplo {:ok, next_number, new_state}, a máquina deve responder a quem a invoca com o número seguinte e definir o novo estado como seu estado. Se, em vez disso, devolver um tuplo {:error, error}, a máquina deve responder a quem a invoca com o erro e não alterar o seu estado.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:error, :empty_queue}
Implementa a função reset_state/1 e o callback GenServer necessário.
Deve chamar a função TakeANumberDeluxe.State.new/2 para criar um novo estado a partir do min_number e do max_number do estado atual. A máquina deve definir o novo estado como seu estado. Não deve responder a quem a invoca.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.reset_state(machine)
# => :ok
Modifica o arranque da máquina. Deve ler o valor da chave :auto_shutdown_timeout na keyword list passada como init_arg e passá-lo como terceiro argumento a TakeANumberDeluxe.State.new/3. Usa o valor predefinido :infinity se :auto_shutdown_timeout não tiver sido indicado.
Modifica o reinício do estado da máquina para também passar auto_shutdown_timeout a TakeANumberDeluxe.State.new/3.
Modifica os valores devolvidos por todos os callbacks implementados (init/1 e todos os callbacks handle_*) para definirem um timeout. Usa o valor da chave :auto_shutdown_timeout do estado atual da máquina. Não adiciones o timeout ao valor devolvido {:stop, reason} de init/1: os timeouts só se aplicam depois de o servidor ter iniciado o seu ciclo de receção.
Implementa um callback GenServer para tratar a mensagem :timeout que será enviada à máquina se esta não receber outras mensagens dentro do timeout indicado. Deve terminar o processo com o motivo :normal.
Certifica-te também de que tratas quaisquer mensagens inesperadas, ignorando-as.
{:ok, machine} =
TakeANumberDeluxe.start_link(
min_number: 1,
max_number: 10,
auto_shutdown_timeout: :timer.hours(2)
)
# after 3 hours...
TakeANumberDeluxe.queue_new_number(machine)
# => ** (exit) exited in: GenServer.call(#PID<0.171.0>, :queue_new_number, 5000)
# ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
# (elixir 1.13.0) lib/gen_server.ex:1030: GenServer.call/3
Inscreve-te no Exercism para aprenderes e dominares Elixir com 58 conceitos168 exercícios, e mentoria humana real, tudo grátis.