Visão geral sobre Elixir para Elixir Carajás
Mix.install([
{:kino, "~> 0.16.0"}
])
.
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, [])