Trilhas
/
Elixir
Elixir
/
Exercícios
/
Pegue uma senha Deluxe
Pegue uma senha Deluxe

Pegue uma senha Deluxe

Exercício de aprendizagem

Introdução

GenServer

GenServer (servidor genérico) é um comportamento que abstrai interações comuns cliente-servidor entre processos 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. Ele facilita manter estado e executar código assíncrono.

Note

Fique atento: o nome GenServer é carregado de significados. Ele 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 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 do seu app 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 aprender o que cada callback significa.

Exemplo

Este é um exemplo de servidor que consegue responder às perguntas repetitivas de passageiros irritantes durante uma longa viagem de carro, mais precisamente à pergunta: "já chegamos?". Ele mantém o controle de quantas vezes essa pergunta foi feita, retornando 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 sobre 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 para o callback init/1.
  • Aceitam um terceiro argumento opcional com opções avançadas para executar o processo, que não veremos agora.

Iniciar um servidor chamando GenServer.start/3 ou GenServer.start_link/3 invocará 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 normalmente retorna um destes valores:

  • {:ok, state}. O servidor iniciará 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 iniciará seu laço de recebimento. O processo será encerrado com o motivo fornecido.

Também existem possibilidades mais avançadas que não veremos 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 a 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 normalmente 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 para quem chamou. O terceiro elemento da tupla, state, é o novo estado do servidor após tratar essa mensagem.

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

Note

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

Se essa pessoa estiver disponível, você receberá 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 normalmente retorna uma tupla de 2 elementos {:noreply, state}.

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

Note

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

Se você jogar uma mensagem em uma garrafa no mar, você não espera receber uma resposta imediatamente, ou talvez nunca.

Devo usar call ou cast?

Quase sempre use call, mesmo que 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 maneira de ter certeza de que o servidor recebeu e tratou a mensagem do cliente.

handle_info/2

Mensagens também podem acabar na caixa de correio do servidor por outros meios além de chamar GenServer.call/2 ou GenServer.cast/2, por exemplo, chamando a função send/2 simples.

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 que captura tudo de handle_info/2 que registra erros sobre mensagens inesperadas. Se você substituir essa implementação padrão, certifique-se de sempre incluir sua própria implementação que captura tudo. Se você esquecer, o servidor vai travar se receber uma mensagem inesperada.

Timeouts

O valor de retorno de cada um dos quatro callbacks descritos acima pode ser estendido por mais um elemento na 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 se passado sem nenhuma mensagem chegar, handle_info/2 é chamado com :timeout como primeiro argumento.

Instruções

A máquina básica Take-A-Number estava vendendo muito bem, mas alguns usuários reclamavam da falta de recursos avançados em comparação com outros modelos disponíveis no mercado.

O fabricante ouviu o feedback dos usuários e decidiu lançar um modelo deluxe com mais recursos, e mais uma vez você ficou responsável por escrever o software dessa máquina.

Os novos recursos adicionados ao modelo deluxe incluem:

  • Manter o controle dos números que estão na fila no momento.
  • Definir o número mínimo e o número máximo. Isso permite usar várias máquinas Take-A-Number deluxe para enfileirar clientes de departamentos diferentes de uma mesma instalação, e distinguir os departamentos pela faixa de números.
  • Permitir que certos números furem a fila, para oferecer atendimento prioritário a gestantes e idosos.
  • Desligamento automático, para evitar deixar a máquina ligada sem querer durante todo o fim de semana e desperdiçar energia.

A lógica de negócios da máquina já foi implementada pelo seu colega e pode ser encontrada no módulo TakeANumberDeluxe.State. Agora sua tarefa é envolvê-la em um GenServer.

1. Iniciar a máquina

Use o behaviour GenServer no módulo TakeANumberDeluxe.

Implemente a função start_link/1 e o callback GenServer necessário.

O argumento passado para start_link/1 é uma keyword list. Ela contém as chaves :min_number e :max_number. Os valores sob essas chaves precisam ser passados para a função TakeANumberDeluxe.State.new/2.

Se TakeANumberDeluxe.State.new/2 retornar uma tupla {:ok, state}, a máquina deve iniciar, usando o estado retornado como seu estado. Se ela retornar uma tupla {:error, error}, a máquina deve parar, usando o erro retornado como motivo da parada.

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}

Você deve ter notado que a função TakeANumberDeluxe.State.new/2 também aceita um terceiro argumento opcional, auto_shutdown_timeout. Vamos usá-lo no último passo deste exercício.

2. Relatar o estado da máquina

Implemente a função report_state/1 e o callback GenServer necessário. A máquina deve responder ao chamador com 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,
#    }

3. Enfileirar novos números

Implemente a função queue_new_number/1 e o callback GenServer necessário.

Ela 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 retornar uma tupla {:ok, new_number, new_state}, a máquina deve responder ao chamador com o novo número e definir o novo estado como seu estado. Se ela retornar uma tupla {:error, error}, a máquina deve responder ao chamador com o erro e não alterar 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}

4. Atender o próximo número da fila

Implemente a função serve_next_queued_number/2 e o callback GenServer necessário.

Ela deve chamar a função TakeANumberDeluxe.State.serve_next_queued_number/2 com o estado atual da máquina e seu segundo argumento opcional, priority_number.

Se TakeANumberDeluxe.State.serve_next_queued_number/2 retornar uma tupla {:ok, next_number, new_state}, a máquina deve responder ao chamador com o próximo número e definir o novo estado como seu estado. Se ela retornar uma tupla {:error, error}, a máquina deve responder ao chamador com o erro e não alterar 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}

5. Redefinir o estado

Implemente a função reset_state/1 e o callback GenServer necessário.

Ela deve chamar a função TakeANumberDeluxe.State.new/2 para criar um novo estado usando o min_number e o max_number do estado atual. A máquina deve definir o novo estado como seu estado. Ela não deve responder ao chamador.

{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)

TakeANumberDeluxe.reset_state(machine)
# => :ok

6. Implementar o desligamento automático

Modifique a inicialização da máquina. Ela deve ler o valor sob a chave :auto_shutdown_timeout na keyword list passada como init_arg e passá-lo como terceiro argumento para TakeANumberDeluxe.State.new/3. Use o valor padrão :infinity se :auto_shutdown_timeout não tiver sido informado.

Modifique a redefinição do estado da máquina para também passar auto_shutdown_timeout para TakeANumberDeluxe.State.new/3.

Modifique os valores de retorno de todos os callbacks implementados (init/1 e todos os callbacks handle_*) para definir um timeout. Use o valor sob a chave :auto_shutdown_timeout no estado atual da máquina. Não adicione o timeout ao valor de retorno {:stop, reason} de init/1: timeouts só se aplicam depois que o servidor iniciou seu loop de recebimento.

Implemente um callback GenServer para tratar a mensagem :timeout que será enviada à máquina se ela não receber nenhuma outra mensagem dentro do timeout informado. Ela deve encerrar o processo com o motivo :normal.

Certifique-se de também tratar 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
Editar via GitHub O link abre em uma nova janela ou aba
Elixir Exercism

Tudo pronto para começar Pegue uma senha Deluxe?

Crie sua conta no Exercism para aprender e dominar Elixir com 58 conceitos168 exercícios e mentoria humana de verdade, tudo de graça.