Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Visão geral sobre Elixir para Elixir Carajás

elixir_at_elixir_carajas.livemd

Visão geral sobre Elixir para Elixir Carajás

Mix.install([
  {:kino, "~> 0.16.0"}
])

.

Run in Livebook

Process

Na máquina virtual do Erlang, todo código roda dentro de uma unidade de execução chamada “processo”.

Esse processo que roda dentro da VM do Erlang não é um processo do sistema operacional

Um processo dentro do contexto da VM do Erlang é muito leve, podemos criar literalmente milhões deles dentro de uma instância da VM.

# Vamos criar 1 milhão de processos

for _ <- 1..1_000_000 do
  spawn(fn -> :ok end)
end

Comunicação entre processos

Processos se comunicam por passagem de mensagens, através das funções send e receive.

# inicia um novo processo
child_process_pid =
  spawn(fn ->
    receive do
      {:ping, caller} ->
        IO.puts("Processo #{inspect(self())} recebeu uma mensagem 'ping' do processo #{inspect(caller)}")

        send(caller, {:pong, self()})

        IO.puts(
          "Processo #{inspect(self())} enviou uma mensagem 'pong' para o processo #{inspect(caller)}"
        )
    end
  end)

# pega o pid do processo rodando
parent_process_pid = self()

# envia uma mensagem para o processo child_process_pid
send(child_process_pid, {:ping, parent_process_pid})

IO.puts(
  "Processo #{inspect(parent_process_pid)} enviou uma mensagem 'ping' para o processo #{inspect(child_process_pid)}"
)

# fica esperando receber uma mensagem
receive do
  {:pong, caller} ->
    IO.puts("Processo #{inspect(self())} recebeu uma mensagem 'pong' do processo #{inspect(caller)}")
end

O Livebook nos ajuda a visualizar a troca de mensagens entre processos:

Kino.Process.render_seq_trace(fn ->
  # inicia um novo processo
  child_process_pid =
    spawn(fn ->
      receive do
        {:ping, caller} -> send(caller, :pong)
      end
    end)

  # pega o pid do processo rodando
  parent_process_pid = self()

  # envia uma mensagem para o processo child_process_pid
  send(child_process_pid, {:ping, parent_process_pid})

  receive do
    :pong -> "recebi pong"
  end
end)

Vamos ver outro exemplo de visualização de troca de mensagem, dessa vez um pouco mais complexo.

Kino.Process.render_seq_trace(fn ->
  1..4
  |> Task.async_stream(
    fn _ -> Process.sleep(Enum.random(100..300)) end,
    max_concurrency: 4
  )
  |> Stream.run()
end)

Supervisor

Vamos criar dois processos bem simples usando o módulo de “processos genéricos” da biblioteca padrão chamado GenServer.

defmodule Web do
  use GenServer

  def start_link(_) do
    GenServer.start_link(Web, [], name: Web)
  end

  def init([]) do
    IO.puts("Processo Web foi iniciado")

    {:ok, []}
  end
end
defmodule DB do
  use GenServer, restart: :transient

  def start_link(_) do
    GenServer.start_link(DB, [], name: DB)
  end

  def init([]) do
    IO.puts("Processo DB foi iniciado")

    {:ok, []}
  end
end

Vamos iniciar um supervisor e a partir dele iniciar e supervisionar dois processos, um Web e um DB.

{:ok, my_supervisor_pid} =
  Supervisor.start_link(
    [Web, DB],
    strategy: :one_for_one,
    name: :my_supervisor
  )

my_supervisor_pid

Vamos pegar o PID (process id) do processo DB.

db_original_pid = Process.whereis(DB)

Agora, vamos simular que o processo DB morreu com uma causa fora do normal.

Para isso vamos desligar esse processo com a função GenServer.stop, passando o nome do processo DB e usando o motivo :abnormal_reason

GenServer.stop(DB, :abnormal_reason)

Podemos confirmar que o processo DB original está morto.

Process.alive?(db_original_pid)

Podemos perceber que o PID do processo DB mudou, porque ele foi desligado e depois reiniciado pelo seu supervisor.

db_pid_after_restart = Process.whereis(DB)

IO.puts("PID antigo do processo DB: #{inspect(db_original_pid)}")
IO.puts("PID novo do processo DB: #{inspect(db_pid_after_restart)}")

Porém, se desligarmos o processo DB de modo normal, seu supervisor não irá reiniciá-lo, porque configuramos o módulo do DB com a configuração restart: :transient, que indica que processos desse módulo só serão reiniciados pelo seu supervisor se forem terminados de modo fora do normal.

GenServer.stop(DB, :normal)

Podemos visualizar a árvore de supervisão do supervisor my_supervisor e confirmar que o processo DB não foi reiniciado pelo seu supervisor.

Kino.Process.render_sup_tree(my_supervisor_pid)

Application

Uma Application em Elixir é um modo de “empacotar” um conjunto de processos e sua árvore de supervisão.

Um sistema rodando na VM do Erlang costuma ter diversas Applications, que se comportam como componentes que podem ser iniciados e desligados de modo independente.

Vamos listar as Applications que estão rodando.

Application.started_applications()

Podemos utilizar o Livebook para visualizar a árvore de supervisão de uma aplicação. Por exemplo, da aplicação kino

Kino.Process.render_app_tree(:kino)

Distributed

Location transparency

Para seguir esta seção do notebook, você precisará ter Elixir instalado na sua máquina. Para instalar o Elixir, siga as instruções no site da linguagem.


Quando um processo envia uma mensagem para outro processo, ele não se importa se o outro processo está rodando na mesma máquina ou em outra máquina rodando a Erlang VM.

Esta é uma propriedade da VM do Erlang chamada location transparency.

Vamos ver isso funcionando.

Primeiro, vamos ver um exemplo de troca de mensagens entre dois processos rodando no mesmo nó.

# inicia um novo processo
child_process_pid =
  spawn(fn ->
    receive do
      :ping ->
        IO.puts("Nó do processo que recebeu mensagem: #{node()}")
    end
  end)

# envia uma mensagem para o processo child_process_pid
send(child_process_pid, :ping)

IO.puts(
  "Nó do processo que enviou mensagem:  #{node()}"
)

Kino.nothing()

Agora, vamos conectar o node (nó) rodando o código deste notebook a um outro nó rodando uma VM do Erlang.

Para isso, abra um terminal na sua máquina (assumindo que ela tem o Elixir instalado) e digite:

iex --name remote_node@127.0.0.1 --cookie secret

Esse comando irá iniciar o shell do Elixir (iex) dentro de um nó da Erlang VM, configurando o nome do node como remote_node e o valor do cookie para secret.

Iniciado o iex na sua máquina, execute a célula abaixo para conectar o node onde está rodando este notebook com o node onde está rodando o iex.

remote_node = :"remote_node@127.0.0.1"
remote_cookie = :secret

Node.set_cookie(remote_cookie)
Node.connect(remote_node)

Agora com os nós conectados, poderemos fazer um experimento que irá mostrar como a troca de mensagens entre dois processos independe do nó onde os procesos estão rodando.

Vamos enviar uma mensagem para um processo rodando no node do IEx, o remote_node.

# inicia um novo processo
child_process_pid =
  Node.spawn(remote_node, fn ->
    receive do
      :ping ->
        IO.puts("Nó do processo que recebeu mensagem: #{node()}")
    end
  end)

# envia uma mensagem para o processo child_process_pid
send(child_process_pid, :ping)

IO.puts(
  "Nó do processo que enviou mensagem:  #{node()}"
)

Kino.nothing()

Chamando uma função de um módulo definido em um outro nó

Vamos aprender como rodar uma função de um módulo definido em outro nó.

Primeiro, copie e cole o seguinte módulo no IEx que você está executando no seu terminal:

defmodule Distributed do
  def hello_world do
    IO.puts("Hello world from another node!")
  end
end

Agora vamos usar o módulo de remote procedure call da biblioteca padrão do erlang, o :erpc para executar a função hello_world, do módulo Distributed no nó remote_node.

:erpc.call(remote_node, Distributed, :hello_world, [])