Modelo de Atores
Se você leu a primeira parte do guia, deve se lembrar das propriedades de um processo:
processos têm estado privado e não compartilham memória
processos se comunicam uns com os outros por envio de mensagens (IPC)
processos têm um identificador único no sistema (PID)
Por conta destas propriedades, um processo não está sujeito aos memos problemas de race conditions igual à s threads - embora seja possÃvel se processos utilizarem mecanismos de compartilhamento de memória de forma explÃcita.
Com isto, podemos pensar numa possÃvel abstração para trabalharmos com threads sem precisarmos recorrer ao uso de locks.
É como se tivéssemos "threads especiais" que iriam possuir as mesmas propriedades de um processo: estado privado, identificador único e comunicação por envio de mensagens.
Você acertou, estamos falando do modelo de atores.
Muito bacana isso, Leandro. O kernel fornece tal estrutura?
Não. Temos de criar nossa própria abstração, a nÃvel de user space (runtime).
Existem diversas implementações de modelo de atores em diferentes linguagens de programação. Pra mencionar alguns casos, em Java, temos implementação de modelo de atores com a biblioteca Akka. Em Erlang, a modelagem já faz parte das estruturas internas do runtime. E em Ruby (a partir da versão 3+), temos os Ractors.
Funcionamento do modelo de atores
Para entendermos o funcionamento do modelo de atores e como este se relaciona com Ruby, vamos pensar em como poderÃamos implementar nosso próprio modelo. Não é difÃcil, acredite.

No exemplo acima, repare que, diferente da abstração de Thread que compartilha memória, um Ator não compartilha memória com outros atores. Isto por si só elimina problemas inerentes a condições de corrida.
Acho que entendi, Leandro. Então quer dizer que todo ator usa uma kernel thread por trás?
Depende. Algumas implementações podem fazer com que cada ator seja mapeado diretamente para uma kernel thread, enquanto que em outras, um ator é uma abstração bastante leve dentro do runtime, que faz a multiplexação de atores para kernel threads conforme outros critérios, diminuindo assim o overhead com a criação de kernel threads.
Neste tópico, para fins didáticos, vamos mapear cada ator diretamente para uma kernel thread.
1. O ator deve ter estado privado
Para representar o ator, vamos mapear para uma thread:
actor = Thread.new do
# Lógica do ator aqui
end
actor.value # nil
O método .value
faz a mesma coisa que o .join
, mas traz o valor da última expressão dentro do bloco.
Por enquanto o ator não tem nenhuma lógica implementada. Vamos iniciar o estado dentro da thread:
actor = Thread.new do
state = 41
# Faz coisas...
state += 1
state
end
actor.value # 42
A variável state
foi criada apenas dentro do escopo da thread. Se tentarmos acessar a variável no escopo fora da thread, o programa lança uma exceção:
state
# undefined local variable or method `state' for main:Object (NameError)
Para além disso, podemos passar argumentos para a thread no momento da criação, e receber os argumentos dentro do escopo do bloco da thread:
actor = Thread.new(41) do |state|
state += 1
state
end
actor.value # 42
Foi passado para a thread o valor absoluto 41
, mas podemos também passar variáveis:
balance = 41
actor = Thread.new(balance) do |state|
state += 1
state
end
actor.value # 42 - a thread modificou seu estado interno
balance # 41 - Wow! a thread não modificou o valor original de `balance`
Repare atentamente que a variável balance
criada fora do escopo da thread não foi modificada, apesar da thread ter modificado seu estado interno. É isto que precisamos. Contudo, o quê aconteceu de fato ali?
O Ruby, para alguns tipos de dados primitivos incluindo números inteiros, faz a cópia quando estes são enviados como argumentos para métodos, ou seja, a passagem é feita por valor.
Mas para outros tipos, como os arrays e hashes, não é feita a cópia mas sim a passagem por referência, pelo que a thread iria modificar o valor original, estando sujeita à race condition.
Vamos a um exemplo de um ator que adiciona um elemento em um array:
inbox = []
actor = Thread.new(inbox) do |queue|
queue.push(42)
end
inbox # [42] - Ouch! O array original foi modificado pela thread! Not good...
Não é isto que queremos. O nosso "ator" está sendo capaz de modificar variáveis que foram criadas fora de seu escopo. E se forçarmos uma cópia explÃcita do array para a thread? Há solução pra isso em Ruby, com o método .dup
:
inbox = []
# No momento do dup, é literalmente feita uma cópia do array inteiro e passada
# como argumento para a thread
actor = Thread.new(inbox.dup) do |queue|
queue.push(42)
end
inbox # []
Prontinho, já conseguimos cumprir com o primeiro requisito para modelo de atores: estado privado. Vamos ver o próximo requisito.
2. O ator deve ter identificação única
Para que atores conversem uns com os outros, é preciso que cada um tenha uma identificação única, assim como os processos no SO têm PID.
Em Ruby, cada objeto tem um ID, e com Thread não é diferente:
t1 = Thread.new {}
t2 = Thread.new {}
t3 = Thread.new {}
t1.object_id # 1594340
t2.object_id # 1594341
t3.object_id # 1594342
Yay! Já temos o segundo requisito cumprido. Vamos ao terceiro requisito e não menos importante: envio de mensagens.
3. O ator deve se comunicar por envio de mensagens
Levando em conta que a nossa implementação de ator é uma abstração em cima da Thread, como enviar mensagens para o ator? A classe Thread
não fornece uma forma de enviar mensagens, então temos que criar o nosso próprio mecanismo.
actor = Thread.new do
# Implementação
end
# undefined method `send_message' for #<Thread (irb):1 run> (NoMethodError)
actor.send_message("Hello")
Pois é, simplesmente não funciona...não há nada na implementação de Thread que permita o envio de mensagens, pois a thread é apenas uma abstração em cima de kernel threads que são automaticamente escalonadas pelo sistema operacional.
Para implementar o envio de mensagens entre atores, vamos antes entender duas caracterÃsticas principais sobre o envio: sÃncrono e assÃncrono.
3.1. Envio de mensagens sÃncrono
Num modelo sÃncrono, um processo Sender precisa enviar uma mensagem a outro processo Receiver:


Enquanto o receptor não confirma que recebeu e processou a mensagem, o processo que enviou a mensagem não consegue fazer outra tarefa, portanto fica bloqueado.
Mas o quê acontece caso o receptor este indisponÃvel ou não tenha conseguido processar a mensagem? O processo Sender fica sem saber se a mensagem foi recebida, e a mensagem é descartada:

Ou seja, além de ter deixado o Sender bloqueado, a mensagem ficou perdida pra sempre. Para implementar modelo de atores, precisamos fazer com que o ator seja capaz de receber mensagens de forma assÃncrona.
3.2. Envio de mensagens assÃncrono
No modelo assÃncrono, a ideia é fazer com que a mensagem caia numa espécie de "caixa de correio" - similar ao que temos na vida real, ou então à caixa de entrada de email -, pelo que o ator fique verificando de tempos em tempos se chegou mensagem nova.
O ator é obrigado a responder tais mensagens? Não. Ou seja, neste modelo, o processo que envia a mensagem não necessariamente precisa ter uma resposta sÃncrona de que foi enviada, mas precisa ter a garantia de que a mensagem se encontra na caixa de entrrada.

Neste modelo, o processo Sender não fica bloqueado, ou seja o envio de mensagem foi assÃncrono. Como podemos implementar esta "caixa de entrada"?
3.3. Fila de mensagens
Vamos imaginar que neste caso as mensagens precisam ser processadas na ordem em que chegaram, correto? É um modelo onde o primeiro que entra é o primeiro a sair, que em inglês significa first-in, first-out, ou FIFO.
FIFO mesmo, você acertou, vamos implementar esta caixa de entrada com filas!
Que bem sabemos, filas podem ser implementadas com arrays!



Em Ruby, poderÃamos representar nossa caixa de entrada (fila) do ator como simplesmente inbox, e dentro do ator consumir mensagens desta fila com o método pop:
inbox = []
actor = Thread.new(inbox) do |inbox|
inbox.pop
end
actor.value # nil
E para enviar mensagens para a fila a partir do processo principal, temos o método push:
inbox.push(42)
Entretanto, a esta altura o ator já terminou sua execução, portanto precisamos modificar o ator para que fique em loop verificando se há mensagens na inbox:
inbox = []
actor = Thread.new(inbox) do |inbox|
loop do
puts "Message: #{inbox.pop}"
end
end
actor.join
O que temos na saÃda é isto:
Message:
Message:
Message:
Message:
Message:
Message:
Message:
Message:
Message:
....................
Infinitamente. Not good. Queremos que o ator fique meio que suspenso quando não houver mensagens na fila.
Para resolver este problema, precisamos criar uma fila bloqueante, que bloqueia o ator para não ficar gastando CPU desnecessariamente num loop infinito.
3.4. Fila bloqueante (e thread-safe) de mensagens
A implementação de uma fila bloqueante pode ser feita com exclusão mútua, como já vimos em tópicos anteriores, onde:
a thread verifica se há mensagens na fila:
se houver, faz o pop e volta ao inÃcio do loop
se não houver, utiliza mutex para enviar um sinal à thread que precisa ficar suspensa
quando o processo Sender colocar mensagem na fila, é enviado um sinal à thread que está suspensa:
a thread retoma de onde parou, consome mensagem da fila e repete o voltando ao inÃcio do loop


Precisamos recorrer a mutex e condvar para que a fila seja thread-safe, ou seja, a fila precisa ser compartilhada de forma segura entre threads, sem criar condições de corrida.
Este conceito já foi explicado no módulo de Concorrência em C.
Além de Mutex, em Ruby também temos uma abstração para o uso de Conditions:
inbox = [] # Representação da fila
mutex = Mutex.new # Mutex
condvar = ConditionVariable.new # Condition
actor = Thread.new(inbox) do |inbox|
# Thread fica em loop infinito
loop do
mutex.synchronize do
# Aqui, é enviado um "sinal" para a thread ficar em estado de WAIT
# no caso da fila estar vazia
condvar.wait(mutex) if inbox.empty?
# Quando a thread recebe o sinal de WAKE, continua o processamento
puts "Received message: #{inbox.pop}"
end
end
end
mutex.synchronize do
# Adiciona mensagem na fila e envia sinal de WAKE à thread que detém o mutex
inbox.push(42)
condvar.signal
end
# Espera 5 segundos antes de finalizar o programa, a tÃtulo de vermos a mensagem
# ser processada a tempo no terminal
sleep 5
Received message: 42
Yay! Que dia maravilhoso, não é mesmo? Inclusive, podemos implementar uma abstração de Inbox utilizando a técnica milenar de um array, um mutex e uma variável condicional:
class ThreadSae
def initialize
@queue = []
@mutex = Mutex.new
@condvar = ConditionVariable.new
end
def send(message)
@mutex.synchronize do
@queue.push(message)
@condvar.signal
end
end
def receive
@mutex.synchronize do
@condvar.wait(@mutex) if @queue.empty?
@queue.pop
end
end
end
inbox = BlockingQueue.new
inbox.send(42)
inbox.receive # 42
Mas Leandro, sério que temos que implementar nossa própria fila bloqueante em Ruby?
Calma jovem, estamos com sorte hoje! Ruby já fornece uma classe pra isso, a Thread::Queue
, que é totalmente thread-safe:
queue = Thread::Queue.new
actor = Thread.new(queue) do |inbox|
loop do
puts "Received message: #{inbox.pop}"
end
end
queue.push(42)
sleep 3
Okay, agora que vimos os fundamentos de modelo de atores com exemplos em Ruby, já conseguimos cumprir com as principais caracterÃsticas de um ator:
estado privado
identificação única
envio de mensagens através de fila thread-safe
A seguir, vamos trazer uma série de exemplos impementando a abstração de Ator com todas as propriedades que já exploramos.
Um ator que recebe mensagens
Vamos iniciar a implementação com uma classe Ruby bastante simples:
class Actor
end
A seguir, no método initialize definimos a fila de mensagens e a Thread que irá ficar em background processando as mensagens:
class Actor
def initialize
@inbox = Thread::Queue.new
Thread.new do
loop do
# Implementação
end
end
end
end
A implementação do ator é muito simples:
lê mensagem da inbox
processa mensagem:
se "exit", sai do loop e a thread é finalizada, encerrando o ator
em qualquer outro caso, imprime a mensagem recebida
......
Thread.new do
loop do
message = @inbox.pop
case message
when :exit
break
else
puts "Received message: #{message}"
end
end
end
.....
Agora, no modelo de ator definimos um método send
para o envio de mensagens para a fila e outro chamado exit
que encerra o ator:
class Actor
def initialize
@inbox = Thread::Queue.new
Thread.new do
loop do
message = @inbox.pop
case message
when :exit
break
else
puts "Received message: #{message}"
end
end
end
end
def send(message)
@inbox.push(message)
end
def exit
@inbox.push(:exit)
end
end
##############################
actor = Actor.new
actor.send(42) # Envia mensagem para o ator (async)
sleep 3
SaÃda esperada:
Received message: 42
Um ator que recebe mas também envia mensagens
Para além da capacidade de receber mensagens em uma fila inbox, um ator também deve ser capaz de enviar mensagens para o mundo externo, sempre de forma assÃncrona, utilizando uma fila de mensagens.
Mas ao invés de usar a inbox, podemos definir outra fila, que irá representar a caixa de saÃda, ou outbox.

O envio de mensagens deve ser sempre assÃncrono, ou seja, a mensagem deve ser colocada em alguma fila
Para ilustrar isso melhor, vamos modificar o exemplo para representar uma conta bancária, que realiza depósitos e saques, mantendo um estado privado que é o saldo final da conta. Olha só que belezura fica isso em Ruby:
class Account
def initialize
@inbox = Thread::Queue.new # Caixa de entrada
@outbox = Thread::Queue.new # Caixa de saÃda
@balance = 0 # Estado inicial do ator
Thread.new do
loop do
message = @inbox.pop # Consome mensagem
# OMG! Pattern matching é mesmo lindo, não?
case message
in deposit: amount then @balance += amount
in withdraw: amount then @balance -= amount
in :balance then @outbox.push(@balance) # Coloca saldo na caixa de saÃda
end
end
end
end
def deposit(amount)
@inbox.push(deposit: amount)
end
def withdraw(amount)
@inbox.push(withdraw: amount)
end
def balance
@inbox.push(:balance)
@outbox.pop # Lê a mensagem que o ator deixou na caixa de saÃda
end
end
account = Account.new
account.deposit(100)
account.withdraw(50)
puts "Balance is: #{account.balance} (expected: 50)"
Este código dispensa maiores comentários, não acha?
Um ator ultra genérico em Ruby
O nosso ator atual está amarrado com a lógica de conta bancária. E se criarmos uma abstração de modelo de ator que poderia funcionar com qualquer domÃnio de negócio?
Utilizando o conceito maravilhoso de blocks, podemos definir um ator super genérico que repassa os argumentos (estado inicial) e também o bloco de execução dinâmico:
class Actor
def initialize(*args, &block)
# Define as filas de mensagens (entrada e saÃda)
@inbox = Queue.new
@outbox = Queue.new
# Definição da thread em background, que vai avaliar o bloco dinâmico passado para o ator
Thread.new do
# Execução do block e resultado de retorno, enviando os argumentos
# que foram passados para o ator. Estes argumentos definem o "estado"
# do ator e vão ser repassados para o bloco
result = block.call(self, *args)
# Utiliza o retorno do bloco para chamar um método que "transfere"
# o controle assÃncrono, ou seja, coloca o resultado na caixa de saÃda
self.yield(result)
end
# Após a instanciação do objeto do ator, retorna a própria instância para
# ser usada em outro contexto
self
end
end
A seguir, vamos definir 4 métodos no ator:
send
, que coloca mensagem na inboxreceive
, que lê mensagem da inboxyield
, que colcoa mensagem na outboxtake
, que lê mensagem da outbox
Implementação final do ator genérico:
class Actor
def initialize(*args, &block)
@inbox = Queue.new
@outbox = Queue.new
Thread.new do
result = block.call(self, *args)
self.yield(result)
end
self
end
def receive
@inbox.pop
end
def send(element)
@inbox.push(element)
self
end
def yield(element)
@outbox.push(element)
end
def take
@outbox.pop
end
end
E agora, podemos utilizar este ator para diversos cenários, como no caso de uma conta bancária:
account = Actor.new(0) do |instance, balance|
loop do
message = instance.receive
case message
in deposit: value then balance += value.to_i
in withdraw: value then balance -= value.to_i
in :balance then instance.yield(balance)
end
end
end
100.times do
account.send(deposit: 1)
end
account.send(:balance)
balance = account.take
puts "Balance is: #{balance} (expected: 100)"
Ou ainda para outro contexto, como no caso de pontuação em um jogo:
game = Actor.new(0) do |instance, score|
loop do
message = instance.receive
case message
in :score then instance.yield(score)
in :increment then score += 1
end
end
end
42.times do
game.send(:increment)
end
score = game.send(:score).take
puts "Score is: #{score} (expected: 42)"
Que incrÃvel, Leandro! Esse Ruby é mesmo lindo, não?
Sim, Ruby é demais. Mas calma que não paramos por aÃ. A partir da versão 3.0, Ruby trouxe uma abstração por cima da Thread que resolve o problema de race condition sem precisar de mutex, justamente implementando um modelo de atores, através da classe Ractor, que faz exatamente tudo o que implementamos até aqui.
Modelo de atores com Ractors
É isso mesmo, Ruby 3+ traz o conceito de atores prontinho pra gente, bastando definir como será o modelo conforme nosso requisito:
account = Ractor.new(0) do |balance|
loop do
message = Ractor.receive # Lê mensagem da inbox
case message
in :balance then Ractor.yield(balance) # Coloca mensagem na outbox
in deposit: value then balance += value.to_i
in withdraw: value then balance -= value.to_i
end
end
end
account.send({ deposit: 100 }) # Envia mensagem para a inbox do ator
account.send({ withdraw: 50 }) # Envia mensagem para a inbox do ator
account.send(:balance) # Envia mensagem para a inbox do ator
balance = account.take # Lê mensagem da outbox do ator
puts "Balance is: #{balance} (expected: 50)"
Quero reforçar aqui que o Ractor em Ruby ainda está em fase experimental, portanto não deve ser usado em produção.
Entretanto, acredito que muito em breve teremos isto pronto pra produção. Atualmente temos uma excelente oportunidade para contribuir, deixando a implementação de Ractors em Ruby mais robusta ao longo do tempo.
Ractors não acessam valores globais ou fora do contexto
Para que seja thread-safe, uma caracterÃstica muito importante do Ractor é que não acessa valores globais:
LIST = []
Ractor.new { LIST.push(42) }
O que lança um erro:
#<Thread:0x0000000102ec5740 run> terminated with exception (report_on_exception is true):
(irb):35:in `block in <top (required)>': can not access non-shareable objects in constant Object::LIST by non-main Ractor. (Ractor::IsolationError)
Uma forma de resolver é recebendo a lista no bloco, pelo que o Ruby irá copiar o valor de LIST
para dentro do Ractor:
Ractor.new(LIST) { |list| list.push(42) }
# LIST segue como array vazio, pois foi copiado inteiramente para dentro do Ractor
LIST
O mesmo vale para qualquer valor passado. É sempre copiado, e não movido:
original_list = [1, 2, 3]
ractor = Ractor.new(original_list) do |copied_list|
copied_list.push(42) # Modifica a cópia recebida
copied_list
end
puts "Original LIST: #{original_list.inspect}" # Não foi alterada
puts "Modified list inside Ractor: #{ractor.take.inspect}" # A cópia foi alterada
Movendo valores para dentro de um Ractor (alô, Rust!)
Já vimos que não é possÃvel modificar um valor fora do contexto do Ractor pois é feita uma cópia. E se tentarmos enviar a lista como mensagem para o Ractor? Será que conseguimos?
original_list = [1, 2, 3]
ractor = Ractor.new do
list = Ractor.receive
list.push(42)
end
ractor.send(original_list)
original_list # [1, 2, 3] Original não modificado (ainda bem!)
ractor.take # [1, 2, 3, 42] Cópia modificada
O Ractor continua garantindo a integridade dos dados, completamente thread-safe! Entretanto, se quisermos mover a referência da lista para dentro do Ractor, podemos fazer passando o argumento move: true
:
original_list = [1, 2, 3]
ractor = Ractor.new do
list = Ractor.receive
list.push(42)
end
ractor.send(original_list, move: true)
Vamos confirmar como está o valor da lista dentro do Ractor:
ractor.take # [1, 2, 3, 42]
Continua na mesma, então vamos ver o valor original, se foi modificado ou não:
original_list # #<Ractor::MovedObject:0x0000000107265810>
Superb! O valor foi movido, então isto significa que após o move, não podemos mais utilizar o valor original fora do contexto do Ractor, apenas dentro dele.
Desta forma, não foi feita uma cópia, mas sim um move.
Salve Rustaceans, vocês estão se sentindo em casa agora, né?
Filas podem ser construÃdas a partir de atores
Eu sei que você está agora só o meme da Nazaré, mas já vimos que para criar um ator, precisamos apenas de filas. Mas também podemos criar uma fila thread-safe (ou melhor, ractor-safe) baseada em Ractors.
Tudo o que precisamos é que o ator:
fique bloqueado a espera de mensagens (
Ractor.receive
)repasse as mensagens para a caixa de saÃda (
Ractor.yield
)
queue = Ractor.new do
loop do
Ractor.yield(Ractor.receive)
end
end
queue.send(42)
queue.take # 42
No way! Acabamos de implementar uma fila utilizando Ractors! Agora me diga, caro leitor, quem vem primeiro? O ovo ou a galinha? Fica como lição de casa 😄

Tolerância a falhas em Ruby com Ractors
O modelo de atores traz um desafio crucial em sistemas baseados neste modelo. Processos assÃncronos vão falhar. Não é uma questão de se, mas quando.
Quando entramos no mundo assÃncrono com modelo de atores, temos que ter algum grau de tolerência a falhas, ou seja, lidar com erros e estabelecer planos de ação bem definidos para que os atores continuem operantes mesmo com falhas pontuais.
Em Ruby, infelizmente, neste momento não temos nada que implemente uma forma de supervisionar Ractors que falham.
Mas a gente é maluco e implementa a nossa própria supervisão, né?
Com certeza. Antes de encerrar este tópico em Ruby, vamos implementar um modelo muito simples de supervisão.
Um ator que representa uma conta bancária
Ainda no exemplo de uma conta onde é possÃvel realizar depósitos e saques, a seguir definimos como é este modelo utilizando Ractors e o mais puro suco da orientação a objetos:
class Account
def initialize(balance)
@actor = Ractor.new(balance) do |balance|
loop do
message = Ractor.receive
case message
in deposit: amount then balance += amount
in withdraw: amount then balance -= amount
in :balance then Ractor.yield({ balance: balance })
end
end
end
end
def deposit(amount)
@actor.send({ deposit: amount })
end
def withdraw(amount)
@actor.send({ withdraw: amount })
end
def balance
@actor.send(:balance)
@actor.take[:balance]
end
end
uma conta começa com um saldo inicial, enviado no construtor
no construtor, temos a definição do ator que, através dos métodos públicos do objeto
Account
, recebe as mensagens para alterar o estado da conta bancária

Criando um cenário "feliz", vamos manipular a conta bancária:
account = Account.new(0)
account.deposit(100)
account.withdraw(50)
puts "Balance is: #{account.balance} (expected: 50)"
Tudo perfeito até aqui, mostrando o saldo corretamente na saÃda do programa. Agora, vamos adicionar um pouco de entropia, simulando um crash no ator:
# Adicionando mais matches na mensagem da inbox:
case message
in deposit: amount then balance += amount
in withdraw: amount then balance -= amount
in :balance then Ractor.yield({ balance: balance })
in :crash then raise "Crash!" # <------------------------
end
.........
def simulate_crash!
@actor.send(:crash)
end
.........
Simulamos o crash, então a seguir um depósito e a busca do saldo:
account.simulate_crash!
account.deposit(200)
puts "Balance is: #{account.balance} (expected: 250)"
Balance is: 50 (expected: 50)
#<Thread:0x0000000103170468 run> terminated with exception (report_on_exception is true):
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
from actor-supervisor.rb:4:in `loop'
from actor-supervisor.rb:4:in `block in initialize'
<internal:ractor>:698:in `take': thrown by remote Ractor. (Ractor::RemoteError)
from actor-supervisor.rb:28:in `balance'
from actor-supervisor.rb:77:in `<main>'
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
from actor-supervisor.rb:4:in `loop'
from actor-supervisor.rb:4:in `block in initialize'
Uh, oh! No momento de buscar o saldo, o ator já não está mais saudável. Houve um crash que o deixou em estado de erro.
Precisamos então supervisionar este ator, de modo a garantir que após o crash, o funcionamento do ator continue normalmente.
Supervisionando um ator
Reforçando a implementação atual de Account
:
class Account
def initialize(balance)
@actor = Ractor.new(balance) do |balance|
loop do
message = Ractor.receive
case message
in deposit: amount then balance += amount
in withdraw: amount then balance -= amount
in :balance then Ractor.yield({ balance: balance })
in :crash then raise "Crash!"
end
end
end
end
def deposit(amount)
@actor.send({ deposit: amount })
end
def withdraw(amount)
@actor.send({ withdraw: amount })
end
def balance
@actor.send(:balance)
@actor.take[:balance]
end
def simulate_crash!
@actor.send(:crash)
end
end
Para supervisionar um ator, precisamos de outro ator. Se formos pensar no funcionamento deste supervisor, podemos concluir que:
o supervisor é um ator, que:
cria um worker que será supervisionado
fica em loop "monitorando" o worker:
se está OK, salva o estado do worker e o coloca na outbox
se não está OK, cria outro worker com o estado atual do worker que falhou

E o processo principal, tudo o que precisa é buscar na outbox do supervisor uma conta para poder manipular. Usando, again, OOP, conseguimos atingir isto de forma muito intuitiva:
class AccountSupervisor
def initialize
@actor = Ractor.new do
# Define o estado inicial (que é o atual também)
# e uma conta que será o "worker" supervisionado
current_balance = 0
account = Account.new(current_balance)
loop do
begin
# Verifica se o worker está saudável
response = account.ping
if response && response[:msg] == 'PONG'
# Se estiver saudável, atualiza o estado atual do worker no supervisor
# e coloca o worker na caixa de saÃda (outbox)
current_balance = response[:balance]
Ractor.yield(account)
else
# Caso contrário, lança um erro (não vamos tratar este erro por enquanto)
raise "Account is not responding"
end
rescue Ractor::RemoteError, Ractor::ClosedError => e
# Caso o worker não esteja saudável, como por exemplo sofreu um _crash_,
# cria um novo worker utilizando o estado atual
puts "[Supervisor] Account crashed with error: #{e.message}. Restarting..."
account = Account.new(current_balance)
end
end
end
end
# Método público que vai ser utilizado pelo processo princial para
# manipular uma conta (worker)
def account
@actor.take
end
end
Precisamos implementar o método ping
no worker e também adicionar a mensagem no matching de mensagens da inbox:
class Account ......
case message
in deposit: amount then balance += amount
in withdraw: amount then balance -= amount
in :balance then Ractor.yield({ balance: balance })
in :crash then raise "Crash!"
in :ping then Ractor.yield({ msg: 'PONG', balance: balance })
class Account .....
def ping
@actor.send(:ping)
@actor.take
end

Agora, só falta ligar tudo, criando o worker a partir do supervisor:
supervisor = AccountSupervisor.new
supervisor.account.deposit(100)
supervisor.account.withdraw(50)
puts "Balance is: #{supervisor.account.balance} (expected: 50)"
Até aqui tudo normal. Na saÃda conseguimos ver Balance is: 50 (expected: 50)
.
A seguir, simulamos um crash no worker:
supervisor.account.simulate_crash!
So far, so good. Vamos fazer mais um depósito de 200, pelo que queremos que o worker tenha sido reiniciado pelo supervisor e que ao final tenha o saldo atualizado para 250 (50 anteriormente + 200 de agora):
supervisor.account.deposit(200)
puts "Balance is: #{supervisor.account.balance} (expected: 250)"
Balance is: 50 (expected: 50)
#<Thread:0x0000000104e4fd18 run> terminated with exception (report_on_exception is true):
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
from actor-supervisor.rb:4:in `loop'
from actor-supervisor.rb:4:in `block in initialize'
[Supervisor] Account crashed with error: thrown by remote Ractor.. Restarting...
Balance is: 250 (expected: 250)
Após o crash, podemos ver que o worker foi reiniciado e o novo depósito funcionou, sendo somado com o saldo anterior do ator que falhou.
How cool is that?
Está gostando deste trabalho?
Se está gostando e considera colaborar para que mais trabalhos assim sejam feitos, não deixe de entrar para a lista de investidores premium.
"Tá de sacanagem Leandro kk"
É sério, qualquer apoio financeiro via PIX é mais que bem-vindo, inclusive compartilhar o guia é uma excelente forma de apoiar o projeto também!

Ou copia e cola:
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
No próximo tópico, vamos entrar no mundo do I/O não-bloqueante em Ruby. Apertem os cintos, pois a jornada ainda está longe do fim.
Last updated