Técnicas de Concorrência e Gerenciamento de Estado em Elixir com FSM
Zoey de Souza Pessanha
Posted on June 21, 2024
Introdução
Elixir é uma linguagem funcional que roda na máquina virtual BEAM, a mesma do Erlang, famosa por suas capacidades de concorrência e tolerância a falhas. Um dos padrões poderosos que podem ser implementados em Elixir é a Máquina de Estado Finito (FSM - Finite State Machine), combinada com processamento assíncrono. Este artigo explora como esses padrões podem beneficiar programas em Elixir, proporcionando controle sobre o fluxo de eventos e melhorando a eficiência e a robustez dos sistemas.
Máquinas de Estado Finito (FSM)
Uma Máquina de Estado Finito é um modelo computacional que consiste em um número finito de estados, transições entre esses estados, e ações, que podem ser acionadas por eventos. Em um sistema FSM, um objeto pode estar em apenas um estado de cada vez, e ele pode mudar de estado em resposta a um evento. Cada transição pode ser associada a uma ação específica.
Benefícios da FSM
Organização e Clareza: FSM ajuda a organizar e clarear o comportamento de sistemas complexos, tornando o fluxo de eventos mais previsível e fácil de entender.
Controle de Fluxo: Com FSM, o controle do fluxo de estados é explícito e definido claramente, permitindo transições apenas quando condições específicas são atendidas.
Facilidade de Manutenção: FSM permite encapsular a lógica de estado em um único lugar, facilitando a manutenção e atualização do sistema sem introduzir bugs.
Exemplo de FSM
Considere um sistema simples de autorização de pagamento com os seguintes estados: pending
, authorized
, denied
e failed
.
Estado Inicial: O pagamento começa no estado
pending
.Transição para
authorized
: Quando uma solicitação de autorização é aprovada, o estado muda paraauthorized
.Transição para
denied
: Se a solicitação for negada, o estado muda paradenied
.Transição para
failed
: Se houver um erro no processamento, o estado muda parafailed
.
Um fluxo simplificado de FSM poderia ser representado assim:
defmodule PaymentFSM do
@moduledoc """
Define a FSM para estados de pagamento.
"""
use Fsmx.Fsm,
transitions: %{
"pending" => ["authorized", "denied", "failed"],
"authorized" => [],
"denied" => [],
"failed" => []
}
end
Aqui usamos a biblioteca
Fsmx
, mas é possível usar funções simples ou mesmo o ecto para definir uma FSM.
Neste exemplo, um pagamento pode mudar de pending
para authorized
, denied
, ou failed
. Uma vez que está em authorized
, denied
, ou failed
, ele não pode transitar para outros estados.
Processamento Assíncrono
O processamento assíncrono permite que operações sejam executadas em paralelo ou de maneira não bloqueante, aumentando a eficiência e capacidade de resposta do sistema. Em Elixir, isso é conseguido utilizando processos leves gerenciados pela máquina virtual BEAM.
Benefícios do Processamento Assíncrono
Escalabilidade: O processamento assíncrono permite que múltiplas tarefas sejam executadas simultaneamente, aumentando a escalabilidade do sistema.
Responsividade: Sistemas assíncronos podem continuar a responder a novas requisições enquanto outras operações estão em andamento.
Tolerância a Falhas: A arquitetura de supervisão de Elixir permite que processos falhem e sejam reiniciados sem afetar o restante do sistema.
Exemplo de Processamento Assíncrono
Imagine um sistema que processa eventos de pagamento. Cada evento pode desencadear várias operações, como verificação de saldo, registro de transações, etc. Para não bloquear o processamento de novos eventos, podemos usar tarefas assíncronas.
defmodule PaymentProcessor do
@moduledoc """
Processa eventos de pagamento de forma assíncrona.
"""
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def process_event(event) do
GenServer.cast(__MODULE__, {:process, event})
end
@impl true
def init(:ok) do
{:ok, []}
end
@impl true
def handle_cast({:process, event}, deadletter) do
case handle_event(event) do
:ok -> {:noreply, deadletter}
{:error, reason} -> {:noreply, [event | deadletter]}
end
end
# dependendo da complexidade, podem existir
# diferentes módulos de handler para eventos
defp handle_event(event) do
# Lógica para processar o evento de pagamento
# Efeitos colaterais
# Observabilidade
IO.puts("Processando evento: #{inspect(event)}")
end
end
Neste exemplo, cada evento é processado em uma nova tarefa, permitindo que o sistema continue a receber e enfileirar novos eventos sem esperar que os eventos anteriores sejam processados.
Lock Otimista
O Lock Otimista é uma técnica utilizada para lidar com concorrência em sistemas distribuídos. Ao contrário do Lock Pessimista, que bloqueia os recursos até que uma transação seja concluída, o Lock Otimista assume que as colisões são raras e permite que múltiplas transações ocorram simultaneamente, verificando conflitos apenas no momento da confirmação.
Benefícios do Lock Otimista
- Melhor Desempenho: Reduz a quantidade de bloqueios, permitindo maior paralelismo.
- Menos Deadlocks: Minimiza a chance de deadlocks, uma vez que os recursos não são bloqueados por longos períodos.
- Maior Escalabilidade: Facilita a escalabilidade horizontal, essencial para sistemas distribuídos de grande porte.
Exemplo de Lock Otimista
Vamos adicionar a lógica de lock otimista em nosso sistema de pagamentos. A ideia é verificar e atualizar o estado do pagamento apenas se ele não foi modificado por outro processo.
defmodule Payments do
alias PaymentSystem.Repo
alias PaymentSystem.Payments.Payment
def update_payment_with_lock(id, attrs) do
Repo.transaction(fn ->
payment = Repo.get!(Payment, id)
updated_payment = Payment.changeset(payment, attrs)
case Repo.update(updated_payment) do
{:ok, _payment} -> :ok
{:error, changeset} -> Repo.rollback(changeset)
end
end)
end
end
Neste exemplo, usamos uma transação para garantir que o pagamento seja atualizado apenas se ele não tiver sido modificado por outro processo desde a última leitura.
Integração de FSM, Lock Otimista e Processamento Assíncrono
Combinar FSM, Lock Otimista e Processamento Assíncrono em Elixir pode levar a sistemas altamente eficientes e robustos. A FSM controla o fluxo de estados, garantindo que as transições sejam ordenadas e previsíveis. O Lock Otimista permite que transações concorrentes sejam gerenciadas eficientemente, enquanto o Processamento Assíncrono garante que operações de longa duração não bloqueiem o sistema.
Exemplo Completo
Vamos combinar os conceitos discutidos anteriormente em um exemplo completo:
defmodule PaymentFSM do
use Fsmx.Fsm,
transitions: %{
"pending" => ["authorized", "denied", "failed"],
"authorized" => [],
"denied" => [],
"failed" => []
}
def start_payment(payment) do
process_event(payment, "start")
end
defp process_event(payment, event) do
PaymentProcessor.process_event(event)
end
defp handle_event(payment, event) do
case transition(payment, event) do
{:ok, new_payment} ->
IO.puts("Evento processado com sucesso: #{inspect(new_payment)}")
{:error, reason} ->
IO.puts("Erro ao processar evento: #{inspect(reason)}")
end
end
defp transition(payment, "start") do
Fsmx.transition(payment, "authorized")
end
end
defmodule PaymentProcessor do
alias PaymentSystem.{Payments, PaymentFSM}
def process_payment_async(payment) do
Task.start(fn -> process_payment(payment) end)
end
defp process_payment(payment) do
with {:ok, payment} <- PaymentFSM.authorize_payment(payment),
{:ok, payment} <- Payments.update_payment_with_lock(payment.id, %{status: "authorized"}) do
IO.puts("Pagamento processado com sucesso: #{inspect(payment)}")
else
{:error, reason} ->
PaymentFSM.fail_payment(payment)
IO.puts("Falha ao processar pagamento: #{inspect(payment)}. Motivo: #{reason}")
end
end
end
Neste exemplo, a FSM gerencia o estado do pagamento, o Lock Otimista garante que as atualizações de estado ocorram de maneira concorrente segura, e o processamento de eventos é feito de forma assíncrona, garantindo que o sistema permaneça responsivo e eficiente.
Conclusão
O uso de Máquinas de Estado Finito (FSM) e processamento assíncrono interno pode trazer inúmeros benefícios para programas em Elixir, desde melhor organização e controle de fluxo até maior resiliência e escalabilidade. Ao combinar esses padrões, é possível criar sistemas robustos, capazes de lidar com complexidade e alta carga de forma eficaz.
Posted on June 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.