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

Visão geral sobre Elixir para Poli-USP computação

elixir_at_poli_comp.livemd

Visão geral sobre Elixir para Poli-USP computação

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

Processes

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

Esse processo que roda dentro da VM do Erlang não é o processo do sistema operacional. Um processo dentro do contexto da VM do Erlang é muito leve, podemos criar literalmente milhões deles dentro de uma VM

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

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

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

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

        send(caller, {:pong, self()})
    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 para o processo #{inspect(child_process_pid)}"
)

# fica esperando receber uma mensagem mensagem
receive do
  {:pong, caller} ->
    IO.puts("Processo #{inspect(self())} recebeu uma mensagem 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 -> :it_worked!
  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)

Supervisors

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

defmodule SimpleProcess do
  use GenServer

  def start_link(name) do
    GenServer.start_link(__MODULE__, name, name: name)
  end

  def child_spec(init_arg) do
    Supervisor.child_spec(
      %{
        id: init_arg,
        start: {__MODULE__, :start_link, [init_arg]},
        restart: :transient
      },
      []
    )
  end

  @impl true
  def init(name) do
    Process.flag(:trap_exit, true)
    IO.puts("Starting SimpleProcess: #{inspect(name)}")

    {:ok, name}
  end

  @impl true
  def terminate(_reason, name) do
    IO.puts("Shutting down SimpleProcess: #{inspect(name)}")

    :ok
  end
end

Vamos iniciar um supervisor e a partir dele iniciar e supervisionar dois processos do módulo SimpleProcess, nomeados por :process_one e :process_two.

{:ok, supervisor_pid} =
  Supervisor.start_link(
    [
      {SimpleProcess, :process_one},
      {SimpleProcess, :process_two}
    ],
    strategy: :one_for_one
  )

supervisor_pid

Vamos pegar o PID (process id) do :process_one.

process_one_original_pid = Process.whereis(:process_one)

Agora, vamos desligar o :process_one e perceber que ele será reiniciado pelo seu supervisor.

GenServer.stop(:process_one, :abnormal_reason)
supervisor_pid

Podemos confirmar que o processo original do :process_one foi desligado.

Process.alive?(process_one_original_pid)

Podemos perceber que o PID do :process_one mudou, porque desligamos ele e ele foi reiniciado pelo seu supervisor

process_pid_after_restart = Process.whereis(:process_one)

IO.puts("Old pid: #{inspect(process_one_original_pid)}")
IO.puts("New pid: #{inspect(process_pid_after_restart)}")

Porém, se desligarmos o process_one de modo normal, seu supervisor não irá reiniciá-lo, porque configuramos o módulo do process_one 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(:process_one, :normal)

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

supervisor_pid

Applications

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 iniciadas e desligadas de modo independente.

Vamos listar as Applications que estão rodando e usar o “Phoenix Dashboard” para investigar a árvore de supervisão de uma dessas aplicações.

Application.started_applications()
node()

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

: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 fisicamente 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ó.

current_node_name = node()

# inicar um novo processo
child_process_pid =
  spawn(fn ->
    receive do
      {:ping, caller} ->
        send(caller, {:pong, node(), self()})
    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)} do nó #{node()} enviou uma mensagem para o processo #{inspect(child_process_pid)}"
)

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

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 --sname remote_node

Esse comando irá iniciar o shell do Elixir dentro de um nó da Erlang VM com o nome remote_node.

Copie e cole o seguinte código para dentro do seu IEx.

IO.puts(node())
IO.puts(Node.get_cookie())

Esse código irá imprimir o nome completo do nó e o seu cookie:

O cookie, dentro do contexto da Erlang VM, é uma espécie de chave de autenticação compartilhada que é usada para permitir que nós da Erlang VM se comuniquem uns com os outros.

Execute a célula a seguir e copie e cole o nome do nó o o seu cookie para os campos que irão aparecer.

Ao executar a célula abaixo:

  • o nome do nó que você iniciou com o IEx será salvo na variável remote_node
  • o nó rodando o código desse notebook irá se conectar com o nó do seu IEx
remote_node =
  Kino.Input.text("Node")
  |> Kino.render()
  |> Kino.Input.read()
  |> String.to_atom()

cookie =
  Kino.Input.text("Cookie")
  |> Kino.render()
  |> Kino.Input.read()
  |> String.to_atom()

Node.set_cookie(remote_node, cookie)

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.

current_node_name = node()

# inicia um novo processo dentro do nó "remote_node"
remote_child_process_pid =
  Node.spawn(remote_node, fn ->
    receive do
      {:ping, caller} ->
        send(caller, {:pong, node(), self()})
    end
  end)

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

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

IO.puts(
  "Processo #{inspect(parent_process_pid)} do nó #{node()} enviou uma mensagem para o processo #{inspect(remote_child_process_pid)}"
)

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

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 bibliotca 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, [])