Concorrência & OTP - Parte 2

Começando com GenServer

Dando continuidade no assunto sobre GenServer, hoje irei focar um pouco mais nos callbacks usando o mesmo projeto iniciado no post anterior.

Um pouco mais sobre callbacks

Callbacks, neste contexto, são funções implementadas por um GenServer. Por exemplo, temos uma função de callback chamada handle_call/3 que serve para lidar com mensagens síncronas.

Ao implementar essa função estamos sobrescrevendo a função padrão handle_call/3 de um GenServer.

Um GenServer possui várias funções de callback, irei demonstrar algumas à medida que for evoluindo o módulo PaymentServer.

Show me the code

Não existe forma melhor de explicar como os callbacks funcionam senão na prática.

handle_call/3

Como dito anteriormente, um GenServer possui um estado temporário. Irei modificar um pouco a função init/1 para armazenar uma listagem de invoices que obteríamos através de uma API, por exemplo.

# lib/payment/payment_server.ex
## ...

def init(args) do
  ## ...

  Process.sleep(3000)
  data = [
    %{uuid: "123-foo", year: 2021, customer: "Agriculture, Forestry and Fishing", value: 9.9,status: "paid"},
    %{uuid: "123-bar", year: 2020, customer: "Agriculture, Forestry and Fishing", value: 29.9,status: "pending"},
    %{uuid: "123-zaz", year: 2020, customer: "Agriculture, Forestry and Fishing", value: 99.9,status: "pending"}
  ]

  state = %{invoice: data}

  {:ok, state}
end
💡 A função Process.sleep(3000) foi adicionada apenas para simular um pequeno delay no momento do fetch dos dados.

Agora, irei implementar a função handle_call/3 para obter a listagem que foi armazenado no state.

# lib/payment/payment_server.ex

#...
#...

def handle_call(:get_state, _from, state) do
  {:reply, state, state}
end

Indo no IEx:

iex -S mix

iex(1)> {:ok, pid} = GenServer.start(PaymentServer, "#myElixirStatus")
{:ok, #PID<0.143.0>}

iex(2)> GenServer.call(pid, :get_state)
%{
  invoices: [
    %{
      customer: "Agriculture, Forestry and Fishing",
      status: "pending",
      uuid: "123-bar",
      value: 29.9,
      year: 2020
    },
    %{
      customer: "Agriculture, Forestry and Fishing",
      status: "pending",
      uuid: "123-zaz",
      value: 99.9,
      year: 2020
    },
    %{
      customer: "Agriculture, Forestry and Fishing",
      status: "pending",
      uuid: "123-zaz",
      value: 99.9,
      year: 2020
    }
  ]
}

Algo a ser observado é o uso da função call/2. Eu gosto de pensar nela como uma interface de baixo nível para interagir com o processo. Uma representação visual de todo fluxo seria:

handle_call Você pode encontrar essa imagem em: https://elixir-lang.org/cheatsheets/gen-server.pdf

Como você pode ver, existem vários valores suportados como retorno para o handle_call.

Dividir para conquistar

Outro ponto a ser observado é um pequeno atraso de 3 segundos assim que o processo é iniciado.

Isso se dá pelo fato da função init/1 ser uma função síncrona.

Isso pode ser facilmente contornado usando o callback handle_continue/2.

# lib/payment/payment_server.ex
# ...

def init(args) do

# ...

{:ok, state, {:continue, :fetch_data} }
end

# ...

def handle_continue(:fetch_data, state) do
  Process.sleep(3000)
  data = [
    %{uuid: "123-foo", year: 2021, customer: "Agriculture, Forestry and Fishing", value: 9.9,status: "paid"},
    %{uuid: "123-bar", year: 2020, customer: "Agriculture, Forestry and Fishing", value: 29.9,status: "pending"},
    %{uuid: "123-zaz", year: 2020, customer: "Agriculture, Forestry and Fishing", value: 99.9,status: "pending"}
  ]

  updated_state = Map.put(state, :invoices, data)

  {:noreply, updated_state}
end

Movendo o fetch de invoices para o handle_continue/2 não haverá mais um atraso na inicialização do processo.

Repo


No código do repositório e para os próximos exemplos acabei movendo o fetch dos dados para módulo FetchData/0 apenas para manter o código do GenServer menor e mais limpo.

Referências:


Comentários